Post on 11-Jul-2020
AndroidDevelopmentwithKotlin
LearnAndroidapplicationdevelopmentwiththeextensivefeaturesofKotlin
MarcinMoskalaIgorWojda
BIRMINGHAM-MUMBAI
AndroidDevelopmentwithKotlinCopyright©2017PacktPublishing
Allrightsreserved.Nopartofthisbookmaybereproduced,storedinaretrievalsystem,ortransmittedinanyformorbyanymeans,withoutthepriorwrittenpermissionofthepublisher,exceptinthecaseofbriefquotationsembeddedincriticalarticlesorreviews.
Everyefforthasbeenmadeinthepreparationofthisbooktoensuretheaccuracyoftheinformationpresented.However,theinformationcontainedinthisbookissoldwithoutwarranty,eitherexpressorimplied.Neithertheauthors,norPacktPublishing,anditsdealersanddistributorswillbeheldliableforanydamagescausedorallegedtobecauseddirectlyorindirectlybythisbook.
PacktPublishinghasendeavoredtoprovidetrademarkinformationaboutallofthecompaniesandproductsmentionedinthisbookbytheappropriateuseofcapitals.However,PacktPublishingcannotguaranteetheaccuracyofthisinformation.
Firstpublished:August2017
Productionreference:1280817
PublishedbyPacktPublishingLtd.LiveryPlace35LiveryStreetBirminghamB32PB,UK.
Credits
Authors
MarcinMoskala
IgorWojda
CopyEditor
SafisEditing
Reviewers
MikhailGlukhikh
StepanGoncharov
ProjectCoordinator
VaidehiSawant
CommissioningEditor
AaronLazar
Proofreader
SafisEditing
AcquisitionEditor
ChaitanyaNair
Indexer
FrancyPuthiry
ContentDevelopmentEditor
RohitKumarSingh
Graphics
AbhinashSahu
TechnicalEditor
PavanRamchandani
ProductionCoordinator
NileshMohite
AbouttheAuthorsMarcinMoskalaisanexperiencedAndroiddeveloperwhoisalwayslookingforwaystoimprove.HehasbeenpassionateaboutKotlinsinceitsearlybetarelease.HewritesarticlesforTradepressandspeaksatprogrammingconferences.
Marcinisquiteactiveintheprogrammingandopensourcecommunityandisalsopassionateaboutcognitiveanddatascience.Youcanvisithiswebsite(marcinmoskala.com),orfollowhimonGitHub(MarcinMoskala)andonTwitter(@marcinmoskala).
Iwouldliketothankmyco-workersinGamekit,Docplanner,andApreel.Iespeciallywanttothankmysupervisors,whowerenotonlysupportive,butwhoarealsoconstantsourceofknowledgeandinspiration:MateuszMikulski,KrzyysztofWolniak,BartekWilczynskiandRafalTrzeciak.IwouldliketothankMarekKaminski,GlebSmirnov,JacekJablonski,andMaciejGorskiforthecorrections,andDariuszBacinskiandJamesShvartsforreviewingthecodeofexampleapplication.AlsoIwouldliketothankmyfamilyandmygirlfriend,MajaMarkiewiczforhersupport,help,makinganenvironmentthatissupportingpassionandself-realization.
IgorWojdaisanexperiencedengineerwithover11yearsofexperienceinsoftwaredevelopment.HisadventurewithAndroidstartedafewyearsago,andheiscurrentlyworkingasaseniorAndroiddeveloperinthehealthcareindustry.IgorhasbeendeeplyinterestedinKotlindevelopmentlongbeforethe1.0versionwasofficiallyreleased,andheisanactivememberoftheKotlincommunity.Heenjoyssharinghispassionforcodingwithdevelopers.
Tolearnmoreabouthim,youcanvisitonMedium(@igorwojda)andfollowhimonTwitter(@igorwojda).
IwouldalsoliketothankamazingteamatBabylon,whoarenotonlyprofessionalsbutalsotheinspiringandveryhelpfulpeople,especiallyMikolajLeszczynski,SakisKaliakoudas,SimonAttard,BalachandarKolathurMani,SergioCarabantes,JoaoAlves,TomasNavickas,MarioSanoguera,SebastienRouif.Iofferthankstoallthereviewers,especiallytechnicalreviewerStepanGoncharov,MikhailGlukhikhandmycolleagueswholivedusfeedbackonthedrafts,especiallyMichałJankowski.Ialsothankfultomyfamilyforalloftheirloveandsupport.I'dliketothankmyparentsforallowingmetofollowmyambitionsthroughoutmychildhoodandforalltheeducation.ThanksalsogotoJetBrainsforcreatingthisawesomelanguageandtotheKotlincommunityforsharingtheknowledge,beinghelpful,openandinspiring.Thisbookcouldnotbewrittenwithoutyou!Iofferspecialthankstomyfriends,especiallyKonradHamela,MarcinSobolski,MaciejGierasimiuk,RafalCupial,MichalMazurandEdytaSkibafortheirfriendship,inspirationandcontinuoussupport.Ivalueyouradviceimmensely.
AbouttheReviewersMikhailGlukhikhhasgraduatedfromIoffePhysicalTechnicalSchoolin1995andfromSaintPetersburgStatePolytechnicalUniversityin2001withmasterdegreeininformationaltechnologies.During2001-2004,hewasPhDstudentinthesameuniversity,andthenhedefendedPhDthesisin2007.ThetitleofhisthesisisSynthesismethoddevelopmentofspecial-purposeinformationalandcontrolsystemswithstructuralredundancy.
MikhailworkedinKodeksSoftwareDevelopmentCenterduring1999-2000,andinEfremovResearchInstituteofElectrophysicalApparatusduring2001-2002.Since2002,heisaleaddeveloperinDigitekLabsatcomputersystemandsoftwareengineeringdepartment.Hewasaseniorlecturerofthedepartmentfrom2004to2007,from2007heisanassociateprofessor.In2013hehadone-yearstayinClausthalUniversityofTechnologyasaninvitedresearcher.In2014,heworkedatSPbofficeofIntelcorporation,sinceMarch2015,heparticipatesinKotlinlanguagedevelopmentatJetBrainscompany.
MikhailisoneofDigitekAegisdefectdetectiontoolauthors,alsoheisoneofDigitekRAtoolauthors.NowadaysprimaryR&Dareasincludecodeanalysis,codeverification,coderefactoringandcodereliabilityestimationmethods.Beforehehadalsointerestsinfault-tolerantsystemdesignandanalysisandalsoinhigh-productivedigitalsignalprocessingcomplexesdeveloping.
StepanGoncharoviscurrentlyworkingatGrabastheengineeringleadoftheDriverAndroidapp.HeisanorganizerofKotlinUserGroupSingaporewhohasdevelopedappsandgamesforAndroidsince2008.HeisaKotlinandRxJavaaddict,andobsessedwithelegantandfunctionalstylecode.Heismainlyfocusedonmobileappsarchitecture.
Stepanismakingadifferencebyspendingmoreandmoretimecontributingtoopen-sourceprojects.HeisthereviewerofLearningRxJava,byThomasNield,
publishedbyPackt.
www.PacktPub.comForsupportfilesanddownloadsrelatedtoyourbook,pleasevisitwww.PacktPub.com.
DidyouknowthatPacktofferseBookversionsofeverybookpublished,withPDFandePubfilesavailable?YoucanupgradetotheeBookversionatwww.PacktPub.comandasaprintbookcustomer,youareentitledtoadiscountontheeBookcopy.
Getintouchwithusatservice@packtpub.comformoredetails.
Atwww.PacktPub.com,youcanalsoreadacollectionoffreetechnicalarticles,signupforarangeoffreenewslettersandreceiveexclusivediscountsandoffersonPacktbooksandeBooks.
https://www.packtpub.com/mapt
Getthemostin-demandsoftwareskillswithMapt.MaptgivesyoufullaccesstoallPacktbooksandvideocourses,aswellasindustry-leadingtoolstohelpyouplanyourpersonaldevelopmentandadvanceyourcareer.
Whysubscribe?FullysearchableacrosseverybookpublishedbyPacktCopyandpaste,print,andbookmarkcontentOndemandandaccessibleviaawebbrowser
CustomerFeedbackThanksforpurchasingthisPacktbook.AtPackt,qualityisattheheartofoureditorialprocess.Tohelpusimprove,pleaseleaveusanhonestreviewonthisbook'sAmazonpageathttps://www.amazon.com/dp/1787123685.
Ifyou'dliketojoinourteamofregularreviewers,youcane-mailusatcustomerreviews@packtpub.com.WeawardourregularreviewerswithfreeeBooksandvideosinexchangefortheirvaluablefeedback.Helpusberelentlessinimprovingourproducts!
TableofContentsPreface
WhatthisbookcoversWhatyouneedforthisbookWhothisbookisforConventionsReaderfeedbackCustomersupport
DownloadingtheexamplecodeErrataPiracyQuestions
1. BeginningYourKotlinAdventureSayhellotoKotlinAwesomeKotlinexamplesDealingwithKotlincode
KotlinPlaygroundAndroidStudio
ConfiguringKotlinfortheprojectUsingKotlininanewAndroidprojectJavatoKotlinconverter(J2K)AlternativewaystorunKotlincode
KotlinunderthehoodTheKotlinstandardlibrary
MorereasonstouseKotlinSummary
2. LayingaFoundationVariablesTypeinferenceStrictnullsafety
SafecallElvisoperatorNotnullassertionLet
NullabilityandJavaCasts
Safe/unsafecastoperator
SmartcastsTypesmartcastsNon-nullablesmartcast
PrimitivedatatypesNumbersCharArraysTheBooleantype
CompositedatatypesStrings
StringtemplatesRangesCollections
StatementsversusexpressionsControlflow
TheifstatementThewhenexpressionLoops
TheforloopThewhileloopOtheriterationsBreakandcontinue
ExceptionsThetry...catchblock
Compile-timeconstantsDelegatesSummary
3. PlayingwithFunctionsBasicfunctiondeclarationandusages
ParametersReturningfunctions
VarargparameterSingle-expressionfunctionsTail-recursivefunctionsDifferentwaysofcallingafunction
DefaultargumentsvaluesNamedargumentssyntax
Top-levelfunctionsTop-levelfunctionsunderthehood
LocalfunctionsNothingreturntypeSummary
4. ClassesandObjectsClasses
ClassdeclarationProperties
Read-writeversusread-onlypropertyPropertyaccesssyntaxbetweenKotlinandJava
IncrementanddecrementoperatorsCustomgetters/setters
ThegetterversuspropertydefaultvalueLate-initializedpropertiesAnnotatingpropertiesInlineproperties
ConstructorsPropertyversusconstructorparameterConstructorwithdefaultarguments
PatternsInheritance
TheJvmOverloadsannotationInterfacesDataclasses
TheequalsandhashCodemethodThetoStringmethodThecopymethodDestructivedeclarations
OperatoroverloadingObjectdeclarationObjectexpressionCompanionobjects
CompanionobjectinstantiationEnumclassesInfixcallsfornamedmethodsVisibilitymodifiers
InternalmodifierandJavabytecodeSealedclassesNestedclassesImportaliasesSummary
5. FunctionsasFirst-ClassCitizensFunctiontype
Whatisfunctiontypeunderthehood?AnonymousfunctionsLambdaexpressions
ImplicitnameofasingleparameterHigher-orderfunctions
ProvidingoperationstofunctionsObserver(Listener)patternCallbackafterathreadedoperation
CombinationofnamedargumentsandlambdaexpressionsLastlambdainargumentconvention
NamedcodesurroundingProcessingdatastructuresusingLINQstyle
JavaSAMsupportinKotlinNamedKotlinfunctiontypes
NamedparametersinfunctiontypeTypealias
UnderscoreforunusedvariablesDestructuringinlambdaexpressionsInlinefunctions
ThenoinlinemodifierNon-localreturnsLabeledreturninlambdaexpressionsCrossinlinemodifierInlineproperties
FunctionReferencesSummary
6. GenericsAreYourFriendsGenerics
TheneedforgenericsTypeparametersversustypearguments
GenericconstraintsNullability
VarianceVariancemodifiersUse-sitevarianceversusdeclaration-sitevarianceCollectionvarianceVarianceproducer/consumerlimitationInvariantconstructor
TypeerasureReifiedtypeparameters
ThestartActivitymethodStar-projectionsTypeparameternamingconventionsSummary
7. ExtensionFunctionsandPropertiesExtensionfunctions
ExtensionfunctionsunderthehoodNomethodoverridingAccesstoreceiverelementsExtensionsareresolvedstatically
CompanionobjectextensionsOperatoroverloadingusingextensionfunctionsWhereshouldtop-levelextensionfunctionsbeused?
ExtensionpropertiesWhereshouldextensionpropertiesbeused?
MemberextensionfunctionsandpropertiesTypeofreceiversMemberextensionfunctionsandpropertiesunderthehood
GenericextensionfunctionsCollectionprocessing
KotlincollectiontypehierarchyThemap,filter,flatMapfunctionsTheforEachandonEachfunctionsThewithIndexandindexedvariantsThesum,count,min,max,andsortedfunctionsOtherstreamprocessingfunctionsExamplesofstreamcollectionprocessingSequence
FunctionliteralswithreceiverKotlinstandardlibraryfunctions
TheletfunctionUsingtheapplyfunctionforinitializationThealsofunctionTherunandwithfunctionThetofunction
Domain-specificlanguageAnko
Summary
8. DelegatesClassdelegation
DelegationpatternDecoratorpattern
PropertydelegationWhataredelegatedproperties?Predefineddelegates
ThelazyfunctionThenotNullfunctionTheobservabledelegateThevetoabledelegatePropertydelegationtoMaptype
CustomdelegatesViewbingingPreferencebindingProvidingadelegate
Summary9. MakingYourMarvelGalleryApplication
MarvelGalleryHowtousethischapterMakeanemptyprojectCharactergallery
ViewimplementationNetworkdefinitionBusinesslogicimplementationPuttingitalltogether
CharactersearchCharacterprofiledisplay
Summary
PrefaceNowadays,theAndroidapplicationdevelopmentprocessisquiteextensive.Overthelastfewyears,wehaveseenhowvarioustoolshaveevolvedtomakeourliveseasier.However,onecoreelementofAndroidapplicationdevelopmentprocesshasn’tchangedmuchovertime,Java.TheAndroidplatformadaptstonewerversionsofJava,buttobeabletousethem,weneedtowaitforaverylongtimeuntilnewAndroiddevicesreachpropermarketpropagation.Also,developingapplicationsinJavacomeswithitsownsetofchallengessinceJavaisanoldlanguagewithmanydesignissuesthatcan’tbesimplyresolvedduetobackwardcompatibilityconstraints.
Kotlin,ontheotherhand,isanewbutstablelanguagethatcanrunonallAndroiddevicesandsolvemanyissuesthatJavacannot.ItbringslotsofprovenprogrammingconceptstotheAndroiddevelopmenttable.Itisagreatlanguagethatmakesadeveloper'slifemucheasierandallowstoproducemoresecure,expressive,andconcisecode.
Thisbookisaneasy-to-follow,practicalguidethatwillhelpyoutospeedupandimprovetheAndroiddevelopmentprocessusingKotlin.WewillpresentmanyshortcutsandimprovementsoverJavaandnewwaysofsolvingcommonproblems.Bytheendofthisbook,youwillbefamiliarwithKotlinfeaturesandtools,andyouwillbeabletodevelopanAndroidapplicationentirelyinKotlin.
WhatthisbookcoversChapter1,BeginningYourKotlinAdventure,discussesKotlinlanguage,itsfeaturesandreasonstouseit.We'llintroducereadertotheKotlinplatformandshowhowKotlinfitsintoAndroiddevelopmentprocess.
Chapter2,LayingaFoundation,islargelydevotedtothebuildingblocksoftheKotlin.Itpresentsvariousconstructs,datatypes,andfeaturesthatmakeKotlinanenjoyablelanguagetoworkwith.
Chapter3,PlayingwithFunctions,explainsvariouswaystodefineandcallafunction.Wewillalsodiscussfunctionmodifiersandlookatpossiblelocationswherefunctioncanbedefined.
Chapter4,ClassesandObjects,discussestheKotlinfeaturesrelatedtoobject-orientedprogramming.Youwilllearnaboutdifferenttypesofclass.Wewillalsoseefeaturesthatimprovereadability:propertiesoperatoroverloadingandinfixcalls.
Chapter5,FunctionsasFirst-ClassCitizens,coversKotlinsupportforfunctionalprogrammingandfunctionsasfirst-classcitizens.Wewilltakeacloserlookatlambdas,higherorderfunctions,andfunctiontypes.
Chapter6,GenericsAreYourFriends,exploresthesubjectsofgenericclasses,interfaces,andfunctions.WewilltakeacloserlookattheKotlingenerictypesystem.
Chapter7,ExtensionFunctionsandProperties,demonstrateshowtoaddnewbehaviortoanexistingclasswithoutusinginheritance.Wewillalsodiscusssimplerwaystodealwithcollectionsandstreamprocessing.
Chapter8,Delegates,showshowKotlinsimplifiesclassdelegationduetobuilt-inlanguagesupport.Wewillseehowtouseitbothbyusingbuilt-inpropertydelegatesandbydefiningcustomones.
Chapter9,MakingYourMarvelGalleryApplication,utilizesmostofthefeatures
discussedinthebookanduseittobuildafullyfunctionalAndroidapplicationinKotlin.
WhatyouneedforthisbookTotestandusethecodepresentedinthisbook,youneedonlyAndroidStudioinstalled.Chapter1,BeginningyourKotlinAdventure,explainshowanewprojectcanbestartedandhowtheexamplespresentedherecanbechecked.Italsodescribeshowmostofthecodepresentedherecanbetestedwithoutanyprograminstalled.
WhothisbookisforTousethisbook,youshouldtobefamiliarwithtwoareas:
YouneedtoknowJavaandobject-orientedprogrammingconcepts,includingobjects,classes,constructors,interfaces,methods,getters,setters,andgenerictypes.So,ifthisareadoesnotringabell,itwillbedifficulttofullyunderstandtherestofthisbook.StartinsteadwithanintroductoryJavabookandreturntothisbookafterward.Thoughnotmandatory,understandingtheAndroidplatformismuchdesirablebecauseitwillhelpyoutounderstandthepresentedexamplesinmoredetail,andyou’llhavedeeperunderstandingtheproblemsthataresolvedbyKotlin.IfyouareanAndroiddeveloperwith6-12monthsofexperienceoryouhavecreatedfewAndroidapplications,you’llbefine.Ontheotherhand,ifyoufeelcomfortablewithOOPconceptsbutyourknowledgeofAndroidplatformislimited,youwillprobablystillbeOKformostofthebook.
Beingopen-mindedandeagertolearnnewtechnologieswillbeveryhelpful.Ifsomethingmakesyoucuriousorcatchesyourattention,feelfreetotestitandplaywithitwhileyouarereadingthisbook
ConventionsInthisbook,youwillfindanumberoftextstylesthatdistinguishbetweendifferentkindsofinformation.Herearesomeexamplesofthesestylesandanexplanationoftheirmeaning.
Codewordsintext,databasetablenames,foldernames,filenames,fileextensions,pathnames,dummyURLs,userinput,andTwitterhandlesareshownasfollows:"Let'slookattherangedatatype,whichallowstodefineend-inclusiveranges."
Ablockofcodeissetasfollows:
valcapitol="England"to"London"
println(capitol.first)//Prints:England
println(capitol.second)//Prints:London
Whenwewishtodrawyourattentiontoaparticularpartofacodeblock,therelevantlinesoritemsaresetinbold:
ext.kotlin_version='1.1.3'
repositories{
maven{url'https://maven.google.com'}
jcenter()
}
Anycommand-lineinputoroutputiswrittenasfollows:
sdkinstallkotlin
Newtermsandimportantwordsareshowninbold.Wordsthatyouseeonthescreen,forexample,inmenusordialogboxes,appearinthetextlikethis:"Setname,package,andlocationforthenewproject.RemembertotickIncludeKotlinsupportoption."
Warningsorimportantnotesappearlikethis.
Tipsandtricksappearlikethis.
ReaderfeedbackFeedbackfromourreadersisalwayswelcome.Letusknowwhatyouthinkaboutthisbook-whatyoulikedordisliked.Readerfeedbackisimportantforusasithelpsusdeveloptitlesthatyouwillreallygetthemostoutof.
Tosendusgeneralfeedback,simplye-mailfeedback@packtpub.com,andmentionthebook'stitleinthesubjectofyourmessage.
Ifthereisatopicthatyouhaveexpertiseinandyouareinterestedineitherwritingorcontributingtoabook,seeourauthorguideatwww.packtpub.com/authors.
CustomersupportNowthatyouaretheproudownerofaPacktbook,wehaveanumberofthingstohelpyoutogetthemostfromyourpurchase.
DownloadingtheexamplecodeYoucandownloadtheexamplecodefilesforthisbookfromyouraccountathttp://www.packtpub.com.Ifyoupurchasedthisbookelsewhere,youcanvisithttp://www.packtpub.com/supportandregistertohavethefilese-maileddirectlytoyou.Youcandownloadthecodefilesbyfollowingthesesteps:
1. Loginorregistertoourwebsiteusingyoure-mailaddressandpassword.2. HoverthemousepointerontheSUPPORTtabatthetop.3. ClickonCodeDownloads&Errata.4. EnterthenameofthebookintheSearchbox.5. Selectthebookforwhichyou'relookingtodownloadthecodefiles.6. Choosefromthedrop-downmenuwhereyoupurchasedthisbookfrom.7. ClickonCodeDownload.
Oncethefileisdownloaded,pleasemakesurethatyouunziporextractthefolderusingthelatestversionof:
WinRAR/7-ZipforWindowsZipeg/iZip/UnRarXforMac7-Zip/PeaZipforLinux
ThecodebundleforthebookisalsohostedonGitHubathttps://github.com/PacktPublishing/Android-Development-with-Kotlin.Wealsohaveothercodebundlesfromourrichcatalogofbooksandvideosavailableathttps://github.com/PacktPublishing/.Checkthemout!
ErrataAlthoughwehavetakeneverycaretoensuretheaccuracyofourcontent,mistakesdohappen.Ifyoufindamistakeinoneofourbooks-maybeamistakeinthetextorthecode-wewouldbegratefulifyoucouldreportthistous.Bydoingso,youcansaveotherreadersfromfrustrationandhelpusimprovesubsequentversionsofthisbook.Ifyoufindanyerrata,pleasereportthembyvisitinghttp://www.packtpub.com/submit-errata,selectingyourbook,clickingontheErrataSubmissionFormlink,andenteringthedetailsofyourerrata.Onceyourerrataareverified,yoursubmissionwillbeacceptedandtheerratawillbeuploadedtoourwebsiteoraddedtoanylistofexistingerrataundertheErratasectionofthattitle.
Toviewthepreviouslysubmittederrata,gotohttps://www.packtpub.com/books/content/supportandenterthenameofthebookinthesearchfield.TherequiredinformationwillappearundertheErratasection.
PiracyPiracyofcopyrightedmaterialontheInternetisanongoingproblemacrossallmedia.AtPackt,wetaketheprotectionofourcopyrightandlicensesveryseriously.IfyoucomeacrossanyillegalcopiesofourworksinanyformontheInternet,pleaseprovideuswiththelocationaddressorwebsitenameimmediatelysothatwecanpursuearemedy.
Pleasecontactusatcopyright@packtpub.comwithalinktothesuspectedpiratedmaterial.
Weappreciateyourhelpinprotectingourauthorsandourabilitytobringyouvaluablecontent.
QuestionsIfyouhaveaproblemwithanyaspectofthisbook,youcancontactusatquestions@packtpub.com,andwewilldoourbesttoaddresstheproblem.
BeginningYourKotlinAdventureKotlinisgreatlanguagethatmakesAndroiddevelopmenteasier,faster,andmuchmorepleasant.Inthischapter,wewilldiscusswhatKotlinreallyisandlookatmanyKotlinexamplesthatwillhelpusbuildevenbetterAndroidapplications.WelcometotheamazingjourneyofKotlin,thatwillchangethewayyouthinkaboutwritingcodeandsolvingcommonprogrammingproblems.
Inthischapter,wewillcoverthefollowingtopics:
FirststepswithKotlinPracticalKotlinexamplesCreatingnewKotlinprojectinAndroidStudioMigratingexistingJavaprojecttoKotlinTheKotlinstandardlibrary(stdlib)WhyKotlinisagoodchoicetolearn
SayhellotoKotlinKotlinisamodern,staticallytyped,Android-compatiblelanguagethatfixesmanyJavaproblems,suchasnullpointerexceptionsorexcessivecodeverbosity.KotlinisalanguageinspiredbySwift,Scala,Groovy,C#,andmanyotherlanguages.KotlinwasdesignedbyJetBrainsprofessionals,basedonanalysisofbothdevelopersexperiences,bestusageguidelines(mostimportantarecleancodeandeffectiveJava),anddataaboutthislanguage'susage.Deepanalysisofotherprogramminglanguageshasbeendone.Kotlintrieshardtonotrepeatthemistakesfromotherlanguagesandtakeadvantageoftheirmostusefulfeatures.WhenworkingwithKotlin,wecanreallyfeelthatthisisamatureandwell-designedlanguage.
Kotlintakesapplicationdevelopmenttoawholenewlevelbyimprovingcodequalityandsafetyandboostingdeveloperperformance.OfficialKotlinsupportfortheAndroidplatformwasannouncedbyGooglein2017,buttheKotlinlanguagehasbeenhereforsometime.IthasaveryactivecommunityandKotlinadoptionontheAndroidplatformisalreadygrowingquickly.WecandescribeKotlinasasafe,expressive,concise,versatile,andtool-friendlylanguagethathasgreatinteroperabilitywithJavaandJavaScript.Let'sdiscussthesefeatures:
Safety:Kotlinofferssafetyfeaturesintermsofnullabilityandimmutability.Kotlinisstaticallytyped,sothetypeofeveryexpressionisknownatcompiletime.Thecompilercanverifythatwhateverpropertyormethodthatwearetryingtoaccessoraparticularclassinstanceactuallyexists.ThisshouldbefamiliarfromJavawhichisalsostaticallytyped,butunlikeJava,Kotlintypesystemismuchmorestrict(safe).Wehavetoexplicitlytellthecompilerwhetherthegivenvariablecanstorenullvalues.ThisallowsmakingtheprogramfailatcompiletimeinsteadofthrowingaNullPointerExceptionatruntime:
Easydebugging:Bugscanbedetectedmuchfasterduringthedevelopmentphaseinsteadofcrashingtheapplicationafteritisreleasedandthusdamagingtheuserexperience.Kotlinoffersaconvenientwaytoworkwithimmutabledata.Forexample,itcandistinguishmutable(read-write)andimmutable(read-only)collectionsbyprovidingconvenientinterfaces(underthehoodcollectionsarestillmutable).Conciseness:MostoftheJavaverbositywaseliminated.Weneedlesscodetoachievecommontasksandthustheamountofboilerplatecodeisgreatlyreduced,evencomparingKotlintoJava8.Asaresult,thecodeisalsoeasiertoreadandunderstand(expressive).Interoperability:KotlinisdesignedtoseamlesslyworksidebysidewithJava(cross-languageproject).TheexistingecosystemofJavalibrariesandframeworksworkswithKotlinwithoutanyperformancepenalties.ManyJavalibrarieshaveevenKotlin-specificversionsthatallowmoreidiomaticusagewithKotlin.KotlinclassescanalsobedirectlyinstantiatedandtransparentlyreferencedfromJavacodewithoutanyspecialsemanticsandviceversa.ThisallowsustoincorporateKotlinintoexistingAndroidprojectsanduseKotlineasilytogetherwithJava(ifwewantto).Versatility:Wecantargetmanyplatforms,includingmobileapplications(Android),server-sideapplications(backend),desktopapplications,frontendcoderunninginthebrowser,andevenbuildsystems(Gradle).
Anyprogramminglanguageisonlyasgoodasitstoolsupport.KotlinhasoutstandingsupportformodernIDEssuchasAndroidStudio,IntelliJIdea,andEclipse.Commontaskslikecodeassistanceorrefactoringarehandledproperly.TheKotlinteamworkshardtomaketheKotlinpluginbetterwitheverysinglerelease.Mostofthebugsarequicklyfixedandmanyofthefeaturesrequestedbythecommunityareimplemented.
Kotlinbugtracker:https://youtrack.jetbrains.com/issues/KTKotlinslackchannel:http://slack.kotlinlang.org/
AndroidapplicationdevelopmentbecomesmuchmoreefficientandpleasantwithKotlin.KotliniscompatiblewithJDK6,soapplicationscreatedinKotlinrunsafelyevenonoldAndroiddevicesthatprecedeAndroid4.
Kotlinaimstobringyouthebestofbothworldsbycombiningconceptsand
elementsfrombothproceduralandfunctionalprogramming.Itfollowsmanyguidelinesaredescribedinthebook,EffectiveJava,2ndEdition,byJoshuaBlochwhichisconsideredmustreadabookforeveryJavadeveloper.
Ontopofthat,Kotlinisopensourced,sowecancheckouttheprojectandbeactivelyinvolvedinanyaspectoftheKotlinprojectsuchasKotlinplugins,compilers,documentationsorKotlinlanguageitself.
AwesomeKotlinexamplesKotlinisreallyeasytolearnforAndroiddevelopersbecausethesyntaxissimilartoJavaandKotlinoftenfeelslikenaturalJavaevolution.Atthebeginning,adeveloperusuallywritesKotlincodehavinginmindhabitsfromJava,butafterawhile,itisveryeasytomovetomoreidiomaticKotlinsolutions.Let'slookatsomecoolKotlinfeatures,andseewhereKotlinmayprovidebenefitsbysolvingcommonprogrammingtasksinaneasier,moreconcise,andmoreflexibleway.Wehavetriedtokeepexamplessimpleandself-explanatory,buttheyutilizecontentfromvariouspartsofthisbook,soit'sfineiftheyarenotfullyunderstoodatthispoint.ThegoalofthissectionistofocusonthepossibilitiesandpresentwhatcanbeachievedbyusingKotlin.Thissectiondoesnotnecessarilyneedtofullydescribehowtoachieveit.Let'sstartwithavariabledeclaration:
varname="Igor"//InferredtypeisString
name="Marcin"
NoticethatKotlindoesnotrequiresemicolons.Youcanstillusethem,buttheyareoptional.Wealsodon'tneedtospecifyavariabletypebecauseit'sinferredfromthecontext.Eachtimethecompilercanfigureoutthetypefromthecontextwedon'thavetoexplicitlyspecifyit.Kotlinisastronglytypedlanguage,soeachvariablehasanadequatetype:
varname="Igor"
name=2//Error,becausenametypeisString
ThevariablehasaninferredStringtype,soassigningadifferentvalue(integer)willresultincompilationerror.Now,let'sseehowKotlinimprovesthewaytoaddmultiplestringsusingstringtemplates:
valname="Marcin"
println("Mynameis$name")//Prints:MynameisMarcin
Weneednomorejoiningstringsusingthe+character.InKotlin,wecaneasilyincorporatesinglevariableorevenwholeexpressionintostringliterals:
valname="Igor"
println("Mynameis${name.toUpperCase()}")
//Prints:MynameisIGOR
InJavaanyvariablecanstorenullvalues.InKotlinstrictnullsafetyforcesustoexplicitlymarkeachvariablethatcanstorenullablevalues:
vara:String="abc"
a=null//compilationerror
varb:String?="abc"
b=null//Itiscorrect
Addingaquestionmarktoadatatype(stringversusstring?),wesaythatvariablecanbenullable(canstorenullreferences).Ifwedon'tmarkvariableasnullable,wewillnotbeabletoassignanullablereferencetoit.Kotlinalsoallowstodealwithnullablevariablesinproperways.Wecanusesafecalloperatortosafelycallmethodsonpotentiallynullablevariables:
savedInstanceState?.doSomething
ThemethoddoSomethingwillbeinvokedonlyifsavedInstanceStatehasanon-nullvalue,otherwisethemethodcallwillbeignored.ThisisKotlin'ssafewaytoavoidnullpointerexceptionsthataresocommoninJava.
Kotlinalsohasseveralnewdatatypes.Let'slookattheRangedatatypethatallowsustodefineendinclusiveranges:
for(iin1..10){
print(i)
}//12345678910
KotlinintroducesthePairdatatypethat,combinedwithinfixnotation,allowsustoholdacommonpairofvalues:
valcapitol="England"to"London"
println(capitol.first)//Prints:England
println(capitol.second)//Prints:London
Wecandeconstructitintoseparatevariablesusingdestructivedeclarations:
val(country,city)=capitol
println(country)//Prints:England
println(city)//Prints:London
Wecaneveniteratethroughalistofpairs:
valcapitols=listOf("England"to"London","Poland"to"Warsaw")
for((country,city)incapitols){
println("Capitolof$countryis$city")
}
//Prints:
//CapitolofEnglandisLondon
//CapitolofPolandisWarsaw
Alternatively,wecanusetheforEachfunction:
valcapitols=listOf("England"to"London","Poland"to"Warsaw")
capitols.forEach{(country,city)->
println("Capitolof$countryis$city")
}
NotethatKotlindistinguishesbetweenmutableandimmutablecollectionsbyprovidingasetofinterfacesandhelpermethods(ListversusMutableList,SetversusSetversusMutableSet,MapversusMutableMap,andsoon):
vallist=listOf(1,2,3,4,5,6)//InferredtypeisList
valmutableList=mutableListOf(1,2,3,4,5,6)
//InferredtypeisMutableList
Immutablecollectionmeansthatthecollectionstatecan'tchangeafterinitialization(wecan'tadd/removeitems).Mutablecollection(quiteobviously)meansthatthestatecanchange.
Withlambdaexpressions,wecanusetheAndroidframeworkbuildinaveryconciseway:
view.setOnClickListener{
println("Click")
}
Kotlinstandardlibrary(stdlib)containsmanyfunctionsthatallowustoperformoperationsoncollectionsinsimpleandconciseway.Wecaneasilyperformstreamprocessingonlists:
valtext=capitols.map{(country,_)->country.toUpperCase()}
.onEach{println(it)}
.filter{it.startsWith("P")}
.joinToString(prefix="CountriesprefixP:")
//Prints:ENGLANDPOLAND
println(text)//Prints:CountriesprefixP:POLAND
.joinToString(prefix="CountriesprefixP:")
Noticethatwedon'thavetopassparameterstoalambda.Wecanalsodefineourownlambdasthatwillallowustowritecodeincompletelynewway.ThislambdawillallowustorunaparticularpieceofcodeonlyinAndroidMarshmallowornewer.
inlinefunsupportsMarshmallow(code:()->Unit){
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.M)
code()
}
//usage
supportsMarshmallow{
println("ThiscodewillonlyrunonAndroidNougatandnewer")
}
WecanmakeasynchronousrequestseasilyanddisplayresponsesonthemainthreadusingthedoAsyncfunction:
doAsync{
varresult=runLongTask()//runsonbackgroundthread
uiThread{
toast(result)//runonmainthread
}
}
Smartcastsallowustowritecodewithoutperformingredundantcasting:
if(xisString){
print(x.length)//xisautomaticallycastedtoString
}
x.length//error,xisnotcastedtoaStringoutsideifblock
if(x!isString)
return
x.length//xisautomaticallycastedtoString
TheKotlincompilerknowsthatthevariablexisofthetypeStringafterperformingacheck,soitwillautomaticallycastittotheStringtype,allowingittocallallmethodsandaccessallpropertiesoftheStringclasswithoutanyexplicitcasts.
Sometimes,wehaveasimplefunctionthatreturnsthevalueofasingleexpression.Inthiscase,wecanuseafunctionwithanexpressionbodytoshortenthesyntax:
funsum(a:Int,b:Int)=a+b
println(sum(2+4))//Prints:6
Usingdefaultargumentsyntax,wecandefinethedefaultvalueforeachfunctionargumentandcallitinvariousways:
funprintMessage(product:String,amount:Int=0,
name:String="Anonymous"){
println("$namehas$amount$product")
}
printMessage("oranges")//Prints:Anonymoushas0oranges
printMessage("oranges",10)//Prints:Anonymoushas10oranges
printMessage("oranges",10,"Johny")
//Prints:Johnyhas10oranges
Theonlylimitationisthatweneedtosupplyallargumentswithoutdefaultvalues.Wecanalsousenamedargumentsyntaxtospecifyfunctionarguments:
printMessage("oranges",name="Bill")
Thisalsoincreasesreadabilitywheninvokingthefunctionwithmultipleparametersinthefunctioncall.
Thedataclassesgiveaveryeasywaytodefineandoperateonclassesfromthedatamodel.Todefineaproperdataclass,wewillusethedatamodifierbeforetheclassname:
dataclassBall(varsize:Int,valcolor:String)
valball=Ball(12,"Red")
println(ball)//Prints:Ball(size=12,color=Red)
Noticethatwehaveareallynice,humanreadablestringrepresentationoftheclassinstanceandwedonotneedthenewkeywordtoinstantiatetheclass.Wecanalsoeasilycreateacustomcopyoftheclass:
valball=Ball(12,"Red")
println(ball)//prints:Ball(size=12,color=Red)
valsmallBall=ball.copy(size=3)
println(smallBall)//prints:Ball(size=3,color=Red)
smallBall.size++
println(smallBall)//prints:Ball(size=4,color=Red)
println(ball)//prints:Ball(size=12,color=Red)
Theprecedingconstructsmakeworkingwithimmutableobjectsveryeasyandconvenient.
OneofthebestfeaturesinKotlinareextensions.Theyallowustoaddnewbehavior(amethodorproperty)toanexistingclasswithoutchangingitsimplementation.Sometimeswhenyouworkwithalibraryorframework,youwouldliketohaveextramethodorpropertyforcertainclass.Extensionsareagreatwaytoaddthosemissingmembers.ExtensionsreducecodeverbosityandremovetheneedtouseutilityfunctionsknownfromJava(forexample,the
StringUtilsclass).Wecaneasilydefineextensionsforcustomclasses,third-partylibraries,orevenAndroidframeworkclasses.Firstofall,ImageViewdoesnothavetheabilitytoloadimagesfromnetwork,sowecanaddtheloadImageextensionmethodtoloadimagesusingthePicassolibrary(animageloadinglibraryforAndroid):
funImageView.loadUrl(url:String){
Picasso.with(context).load(url).into(this)
}
\\usage
imageView.loadUrl("www.test.com\\image1.png")
WecanalsoaddasimplemethoddisplayingtoaststotheActivityclass:
funContext.toast(text:String){
Toast.makeText(this,text,Toast.LENGTH_SHORT).show()
}
//usage(insideActivityclass)
toast("Hello")
Therearemanyplaceswhereusageofextensionswillmakeourcodesimplerandmoreconcise.UsingKotlin,wecanfullytakeadvantageoflambdastosimplifyKotlincodeevenmore.
InterfacesinKotlincanhavedefaultimplementationsaslongastheydon'tholdanystate:
interfaceBasicData{
valemail:String
valname:String
get()=email.substringBefore("@")
}
InAndroid,therearemanyapplicationswherewewanttodelayobjectinitializationuntilitisneeded(used).Tosolvethisproblem,wecanusedelegates:
valretrofitbylazy{
Retrofit.Builder()
.baseUrl("https://www.github.com")
.addConverterFactory(MoshiConverterFactory.create())
.build()
}
Retrofit(apopularAndroidnetworkingframework)propertyinitializationwillbedelayeduntilthevalueisaccessedforthefirsttime.Lazyinitializationmay
resultinfasterAndroidapplicationstartuptimesinceloadingisdeferredtowhenthevariableisaccessed.Thisisagreatwaytoinitializemultipleobjectsinsideaclass,especiallywhennotallofthemarealwaysneeded(forcertainclassusagescenario,wemayneedonlyspecificobjects)orwhennoteveryoneofthemisneededinstantlyafterclasscreation.
AllthepresentedexamplesareonlyaglimpseofwhatcanbeaccomplishedwithKotlin.WewilllearnhowtoutilizethepowerofKotlinthroughoutthisbook.
DealingwithKotlincodeTherearemultiplewaysofmanagingandrunningKotlincode.WewillmainlyfocusonAndroidStudioandKotlinPlayground.
KotlinPlaygroundThefastestwaytotryKotlincodewithouttheneedtoinstallanysoftwareisKotlinPlayground(https://try.kotlinlang.org).WecanrunKotlincodethereusingJavaScriptorJVMKotlinimplementationsandeasilyswitchbetweendifferentKotlinversions.AllthecodeexamplesfromthebookthatdoesnotrequiretheAndroidframeworkdependenciesandcanbeexecutedusingKotlinPlayground.
ThemainfunctionistheentrypointofeveryKotlinapplication.Thisfunctioniscalledwhenanyapplicationstarts,sowemustplacecodefromthebook
examplesinthebodyofthismethod.WecanplacecodedirectlyorjustplaceacalltoanotherfunctioncontainingmoreKotlincode:
funmain(args:Array<String>){
println("Hello,world!")
}
AndroidApplicationshavemultipleentrypoints.mainfunctioniscalledimplicitlybytheAndroidframework,sowecan'tuseittorunKotlincodeonAndroidplatform.
AndroidStudioAllAndroidStudio'sexistingtoolsworkwithKotlincode.Wecaneasilyusedebugging,lintchecks,havepropercodeassistance,refactoringandmore.MostofthethingsworkthesamewayasforJava,sothebiggestnoticeablechangeistheKotlinlanguagesyntax.AllweneedtodoistoconfigureKotlinintheproject.
Androidapplicationshavemultipleentrypoints(differentintentscanstartdifferentcomponentsintheapplication)andrequireAndroidframeworkdependencies.Torunbookexamples,weneedtoextendtheActivityclassandplacecodethere.
ConfiguringKotlinfortheprojectStartingfromAndroidStudio3.0,fulltoolingsupportforKotlinwasadded.InstallationoftheKotlinpluginisnotrequiredandKotlinisintegratedevendeeperintotheAndroiddevelopmentprocess.
TouseKotlinwithAndroidStudio2.x,wemustmanuallyinstalltheKotlinplugin.Toinstallit,weneedtogotoAndroidStudio|File|Settings|Plugins|InstallJetBrainsplugin...|KotlinandpresstheInstallbutton:
TobeabletouseKotlin,weneedtoconfigureKotlininourproject.ForexistingJavaprojects,weneedtoruntheConfigureKotlininprojectaction(theshortcutinWindowsisCtrl+Shift+A,andinmacOS,itiscommand+shift+A)orusethecorrespondingTools|Kotlin|ConfigureKotlininProjectmenuitem:
Then,selectAndroidwithGradle:
Finally,weneedtoselecttherequiredmodulesandtheproperKotlinversion:
TheprecedingconfigurationscenarioalsoappliestoallexistingAndroidprojectsthatwereinitiallycreatedinJava.StartingfromAndroidStudio3.0,wecanalsochecktheIncludeKotlinsupportcheckboxwhilecreatinganewproject:
Inbothscenarios,theConfigureKotlininprojectcommandupdatestherootbuild.gradlefileandthebuild.gradlefilescorrespondingtothemodule(s)byaddingKotlindependencies.ItalsoaddstheKotlinplugintotheAndroidmodule.DuringthetimeofwritingthisbookreleaseversionofAndroidStudio3isnotyetavailable,butwecanreviewbuildscriptfrompre-releaseversion:
//build.gradlefileinprojectrootfolder
buildscript{
ext.kotlin_version='1.1'
repositories{
google()
jcenter()
}
dependencies{
classpath'com.android.tools.build:gradle:3.0.0-alpha9'
classpath"org.jetbrains.kotlin:kotlin-gradle-
plugin:$kotlin_version"
}
}
...
//build.gradlefileintheselectedmodules
applyplugin:'com.android.application'
applyplugin:'kotlin-android'
applyplugin:'kotlin-android-extensions'
...
dependencies{
...
implementation'com.android.support.constraint:constraint-
layout:1.0.2'
}
...
PriortoAndroidPluginforGradle3.x(deliveredwithAndroid
Studio3.0)compiledependencyconfigurationwasusedinsteadofimplementation.
ToupdatetheKotlinversion(letussayinthefuture),weneedtochangethevalueofthekotlin_versionvariableinthebuild.gradlefile(projectrootfolder).ChangesinGradlefilesmeanthattheprojectmustbesynchronized,soGradlecanupdateitsconfigurationanddownloadalltherequireddependencies:
UsingKotlininanewAndroidprojectForthenewKotlinprojectscreatedinAndroidStudio3.x,themainactivitywillbealreadydefinedinKotlin,sothatwecanstartwritingKotlincoderightaway:
AddinganewKotlinfileissimilartoaddingaJavafile.Simplyright-clickonapackageandselectnew|KotlinFile/Class:
ThereasonwhytheIDEsaysKotlinFile/ClassandnotsimplyKotlinclass,analogouslytoJavaclassisthatwecanhavemoremembersdefinedinsideasinglefile.WewilldiscussthisinmoredetailinChapter2,LayingaFoundation.
NoticethatKotlinsourcefilescanbelocatedinsidethejavasourcefolder.WecancreateanewsourcefolderforKotlin,butitisnotrequired:
RunninganddebuggingaprojectisexactlythesameasinJavaanddoesnotrequireanyadditionalstepsbesidesconfiguringKotlinintheproject:
StartingfromAndroidStudio3.0,variousAndroidtemplateswillalsoallowustoselectalanguage.ThisisthenewConfigureActivitywizard:
JavatoKotlinconverter(J2K)MigrationofexistingJavaprojectsisalsoquiteeasy,becausewecanuseJavaandKotlinsidebysideinthesameproject.TherearealsowaystoconvertexistingJavacodeintoKotlincodebyusingtheJavatoKotlinconverter(J2K).
ThefirstwayistoconvertwholeJavafilesintoKotlinfilesusingtheconvertJavaFiletoKotlincommand(keyboardshortcutinWindowsisAlt+Shift+Ctrl+KandinmacOS:option+shift+command+K),andthisworksverywell.ThesecondwayistopasteJavacodeintoanexistingKotlinfileandthecodewillalsobeconverted(adialogwindowwillappearwithaconversionproposition).ThismaybeveryhelpfulwhenlearningKotlin.
Ifwedon'tknowhowtowriteaparticularpieceofcodeinKotlin,wecanwriteitinJava,thensimplycopytotheclipboardandthenpasteitintotheKotlinfile.ConvertedcodewillnotbethemostidiomaticversionofKotlin,butitwillwork.TheIDEwilldisplayvariousintentionstoconvertthecodeevenmoreandimproveitsquality.Beforeconversion,weneedtomakesurethatJavacodeisvalid,becauseconversiontoolsareverysensitiveandtheprocesswillfailevenifasinglesemicolonismissing.TheJ2KconvertercombinedwithJavainteroperabilityallowsKotlinbeintroducedgraduallyintotheexistingproject(forexample,toconvertasingleclassatatime).
AlternativewaystorunKotlincodeAndroidStudiooffersanalternativewayofrunningKotlincodewithouttheneedtorunAndroidapplication.ThisisusefulwhenyouwanttoquicklytestsomeKotlincodeseparatelyfromthelongAndroidcompilationanddeploymentprocess.
ThewaytorunKotlincodeistousebuildKotlinReadEvalPrintLoop(REPL).REPLisasimplelanguageshellthatreadssingleuserinput,evaluatesit,andprintstheresult:
REPLlookslikethecommand-line,butitwillprovideuswithalltherequiredcodehintsandwillgiveusaccesstovariousstructuresdefinedinsidetheproject(classes,interfaces,top-levelfunctions,andsoon):
ThebiggestadvantageofREPLisitsspeed.WecantestKotlincodereallyquickly.
KotlinunderthehoodWewillfocusmainlyonAndroid,butkeepinmindthatKotlincanbecompiledtomultipleplatforms.KotlincodecanbecompiledtoJavabytecodeandthentoDalvikbytecode.HereissimplifiedversionoftheKotlinbuildprocessfortheAndroidplatform:
Afilewitha.javaextensioncontainsJavacodeAfilewitha.ktextensioncontainsKotlincodeAfilewitha.classextensioncontainsJavabytecodeAfilewitha.dexextensioncontainsDalvikbytecodeAfilewitha.apkextensioncontainstheAndroidManifestfile,resources,and.dexfile
ForpureKotlinprojects,onlytheKotlincompilerwillbeused,butKotlinalsosupportscross-languageprojects,wherewecanuseKotlintogetherwithJavainthesameAndroidproject.Insuchcases,bothcompilersareusedtocompiletheAndroidapplicationandtheresultwillbemergedattheclasslevel.
TheKotlinstandardlibraryKotlinstandardlibrary(stdlib)isaverysmalllibrarythatisdistributedtogetherwithKotlin.ItisrequiredtorunapplicationswritteninKotlinanditisaddedautomaticallytoourapplicationduringthebuildprocess.
InKotlin1.1,kotlin-runtimewasrequiredtorunapplicationswritteninKotlin.Infact,inKotlin1.1thereweretwoartifacts(kotlin-runtimeandkotlin-stdlib)thatsharedalotofKotlinpackages.Toreducetheamountofconfusionboththeartifactswillbemergedintosingleartifact(kotlin-stdlib)inintheupcoming1.2versionofKotlin.StartingfromKotlin1.2,kotlin-stdlibisrequiredtorunapplicationswritteninKotlin.
TheKotlinstandardlibraryprovidesessentialelementsrequiredforeverydayworkwithKotlin.Theseinclude:
Datatypeslikearrays,collections,lists,ranges,andsoonExtensionsHigher-orderfunctionsVariousutilitiesforworkingwithstringsandcharsequencesExtensionsforJDKclassesmakingitconvenienttoworkwithfiles,IO,andthreading
MorereasonstouseKotlinKotlinhasstrongcommercialsupportfromJetBrains,acompanythatdeliversverypopularIDEsformanypopularprogramminglanguages(AndroidStudioisbasedonJetBrainsIntelliJIDEA).JetBrainswantedtoimprovethequalityoftheircodeandteamperformance,sotheyneededthelanguagethatwillsolvealltheJavaissuesandprovideseamlessinteroperabilitywithJava.NoneoftheotherJVMlanguagesmeetthoserequirements,soJetBrainsfinallydecidedtocreatetheirownlanguageandstartedtheKotlinproject.Nowadays,Kotlinisusedintheirflagshipproducts.SomeuseKotlintogetherwithJavawhileothersarepureKotlinproducts.
Kotlinisquiteamaturelanguage.Infact,itsdevelopmentstartedmanyyearsbeforeGoogleannouncedofficialAndroidsupport(firstcommitdatesbackto2010-11-08):
TheinitialnameofthelanguagewasJet.Atsomepoint,theJetBrainsteamdecidedtorenameittoKotlin.ThenamecomesfromKotlinIsland,nearSt.PetersburganditsanalogytoJavawhichwasalsonamedaftertheIndonesianisland.
Aftertheversion1.0releasein2016,moreandmorecompaniesstartedtosupporttheKotlinproject.GradleaddedsupportofKotlinintobuildingscripts,Square,thebiggestcreatorofAndroidlibrariespostedthattheystronglysupportKotlinandfinally,Googleannouncedit'sofficialKotlinsupportfortheAndroidplatform.ThismeansthateverytoolthatwillbereleasedbytheAndroidteamwillbecompatiblenotonlywithJavabutalsowithKotlin.GoogleandJetBrainshavebegunapartnershiptocreateanonprofitfoundationforKotlin,responsibleforfuturelanguagemaintenanceanddevelopment.Allofthiswillgreatly
increasethenumberofcompaniesthatwilluseKotlinintheirprojects.
KotlinisalsosimilartoApple'sSwiftprogramminglanguage.Infact,suchistheresemblance,thatsomearticlesfocusondifferences,notsimilarities.LearningKotlinwillbeveryhelpfulfordeveloperseagertodevelopapplicationsforAndroidandiOS.TherearealsoplanstoportKotlintoiOS(Kotlin/Native),somaybewedon'thavetolearnSwiftafterall.FullstackdevelopmentisalsopossibleinKotlin,sowecandevelopserver-sideapplicationsandfrontendclientssharingthesamedatamodelwithmobileclients.
SummaryWe'vediscussedhowtheKotlinlanguagefitsintoAndroiddevelopmentandhowwecanincorporateKotlinintonewandexistingprojects.WehaveseenusefulexampleswhereKotlinsimplifiedthecodeandmadeitmuchsafer.Therearestillmanyinterestingthingstodiscover.
Inthenextchapter,wewilllearnaboutKotlinbuildingblocksandlayafoundationtodevelopAndroidapplicationsusingKotlin.
LayingaFoundationThischapterislargelydevotedtothefundamentalbuildingblocksthatarecoreelementsoftheKotlinprogramminglanguage.Eachonemayseeminsignificantbyitself,butcombinedtogether,theycreatereallypowerfullanguageconstructs.WewilldiscusstheKotlintypesystemthatintroducesstrictnullsafetyandsmartcasts.AlsowewillseeafewnewoperatorsintheJVMworld,andmanyimprovementscomparedtoJava.Wewillalsopresentnewwaystohandleapplicationflowsanddealwithequalityinaunifiedway.
Inthischapter,wewillcoverthefollowingtopics:
Variables,values,andconstantsTypeinferenceStrictnullsafetySmartcastsKotlindatatypesControlstructuresExceptionshandling
VariablesInKotlin,wehavetwotypesofvariables:varorval.Thefirstone,var,isamutablereference(read-write)thatcanbeupdatedafterinitialization.ThevarkeywordisusedtodefineavariableinKotlin.Itisequivalenttoanormal(non-final)Javavariable.Ifourvariableneedstochangeatsometime,weshoulddeclareitusingthevarkeyword.Let'slookatanexampleofavariabledeclaration:
funmain(args:Array<String>){
varfruit:String="orange"//1
fruit="banana"//2
}
1. Createfruitvariableandinitializeitwithvariableorangevalue2. Reinitializefruitvariablewithwithbananavalue
Thesecondtypeofvariableisaread-onlyreference.Thistypeofvariablecannotbereassignedafterinitialization.
Thevalkeywordcancontainacustomgetter,sotechnicallyitcanreturndifferentobjectsoneachaccess.Inotherwords,wecan'tguaranteethatthereferencetotheunderlyingobjectisimmutable:
valrandom:Int
get()=Random().nextInt()
CustomgetterswillbediscussedinmoredetailinChapter4,ClassesandObjects.
ThevalkeywordisequivalentofaJavavariablewiththefinalmodifier.Usingimmutablevariablesisuseful,becauseitmakessurethatthevariablewillneverbeupdatedbymistake.Theconceptofimmutabilityisalsohelpfulforworkingwithmultiplethreadswithoutworryingaboutproperdatasynchronization.Todeclareimmutablevariables,wewillusethevalkeyword:
funmain(args:Array<String>){
valfruit:String="orange"//1
a="banana"//2Error
}
1. Createfruitvariableandinitializeitwithstringorangevalue2. Compilerwillthrowanerror,becausefruitvariablewasalreadyinitialized
Kotlinalsoallowsustodefinevariablesandfunctionsatthelevelofthefile.WewilldiscussitfurtherinChapter3,PlayingwithFunctions.
Noticethatthetypeofthevariablereference(var,val)relatestothereferenceitself,notthepropertiesofthereferencedobject.Thismeansthatwhenusingaread-onlyreference(val),wewillnotbeabletochangethereferencethatispointingtoaparticularobjectinstance(wewillnotbeabletoreassignvariablevalues),butwewillstillbeabletomodifypropertiesofreferencedobjects.Let'sseeitinactionusinganarray:
vallist=mutableListOf("a","b","c")//1
list=mutableListOf("d","e")//2Error
list.remove("a")//3
1. Initializemutablelist2. Compilerwillthrowanerror,becausevaluereferencecannotbechanged
(reassigned)3. Compilerwillallowtomodifycontentofthelist
Thekeywordvalcannotguaranteethattheunderlyingobjectisimmutable.
Ifwereallywanttomakesurethattheobjectwillnotbemodified,wemustuseimmutablereferenceandanimmutableobject.Fortunately,Kotlin'sstandardlibrarycontainsanimmutableequivalentofanycollectioninterface(ListversusMutableList,MapversusMutableMap,andsoon)andthesameistrueforhelperfunctionsthatareusedtocreateinstanceofparticularcollection:
Variable/valuedefinition Referencecanchange Objectstatecanchange
val=listOf(1,2,3)
No No
val=mutableListOf(1,2,3) No Yes
var=listOf(1,2,3) Yes No
var=mutableListOf(1,2,3) Yes Yes
TypeinferenceAswesawinpreviousexamples,unlikeJava,theKotlintypeisdefinedaftervariablename:
vartitle:String
Atfirstglance,thismaylookstrangetoJavadevelopers,butthisconstructisabuildingblockofaveryimportantfeatureofKotlincalledtypeinference.Typeinferencemeansthatthecompilercaninfertypefromcontext(thevalueofanexpressionassignedtoavariable).Whenvariabledeclarationandinitializationisperformedtogether(singleline),wecanomitthetypedeclaration.Let'slookatthefollowingvariabledefinition:
vartitle:String="Kotlin"
ThetypeofthetitlevariableisString,butdowereallyneedanimplicittypedeclarationtodeterminevariabletype?Ontherightsideoftheexpression,wehaveastringKotlinandweareassigningittoavariabletitledefinedontheleft-handsideoftheexpression.
WespecifiedavariabletypeasString,butitwasobvious,becausethisisthesametypeasthetypeofassignedexpression(Kotlin).Fortunately,thisfactisalsoobviousfortheKotlincompiler,sowecanomittypewhendeclaringavariable,becausethecompilerwilltrytodeterminethebesttypeforthevariablefromthecurrentcontext:
vartitle="Kotlin"
Keepinmind,thattypedeclarationisomitted,butthetypeofvariableisstillimplicitlysettoString,becauseKotlinisastronglytypedlanguage.That'swhybothoftheprecedingdeclarationsarethesame,andKotlincompilerwillstillbeabletoproperlyvalidateallfutureusagesofvariable.Hereisanexample:
vartitle="Kotlin"
title=12//1,Error
1. InferredtypewasStringandwearetryingtoassignInt
IfwewanttoassigntheInt(value12)tothetitlevariablethenweneedtospecifytitletypetoonethatisaStringandIntcommontype.Theclosestone,upinthetypehierarchyisAny:
vartitle:Any="Kotlin"
title=12
AnyisanequivalentoftheJavaobjecttype.ItistherootoftheKotlintypehierarchy.AllclassesinKotlinexplicitlyinheritfromtypeAny,evenprimitivetypessuchasStringorInt
Anydefinesthreemethods:equals,toString,andhashCode.Kotlinstandardlibrarycontainsafewextensionsforthistype.WewilldiscussextensionsinChapter7,ExtensionFunctionsandProperties.
Aswecansee,typeinferenceisnotlimitedtoprimitivevalues.Let'slookatinferringtypesdirectlyfromfunctions:
vartotal=sum(10,20)
Intheprecedingexample,theinferredtypewillbethesameastypereturnedbythefunction.WemayguessthatitwillbeInt,butitmayalsobeaDouble,Float,orsomeothertype.Ifit'snotobviousfromthecontextwhattypewillbeinferredwecanuseplacecarrotonthevariablenameandruntheAndroidStudioexpressiontypecommand(forWindows,itisShift+Ctrl+P,andformacOS,itisarrowkey+control+P).Thiswilldisplaythevariabletypeinthetooltip,asfollows:
Typeinferenceworksalsoforgenerictypes:
varpersons=listOf(personInstance1,personInstance2)
//Inferredtype:List<Person>()
AssumingthatwepassonlyinstancesofthePersonclass,theinferredtypewillbeList<Person>.ThelistOfmethodisahelperfunctiondefinedintheKotlinstandardlibrarythatallowustocreatecollection.WewilldiscussthissubjectinChapter7,ExtensionFunctionsandProperties.Let'slookatmoreadvancedexamplesthatusestheKotlinstandardlibrarytypecalledPair,whichcontainsapaircomposedoftwovalues:
varpair="Everest"to8848//Inferredtype:Pair<String,Int>
Intheprecedingexample,apairinstanceiscreatedusingtheinfixfunction,whichwillbediscussedinChapter4,ClassesandObjects,butfornowallweneedtoknowisthatthosetwodeclarationsreturnthesametypeofPairobject:
varpair="Everest"to8848
//Createpairusingtoinfixmethod
varpair2=Pair("Everest",8848)
//CreatePairusingconstructor
Typeinferenceworksalsoformorecomplexscenariossuchasinferringtypefrominferredtype.Let'susetheKotlinstandardlibrary'smapOffunctionandinfixthetomethodofthePairclasstodefinemap.Thefirstiteminthepairwillbeusedtoinferthemapkeytype;thesecondwillbeusedtoinferthevaluetype:
varmap=mapOf("MountEverest"to8848,"K2"to4017)
//Inferredtype:Map<String,Int>
GenerictypeofMap<String,Int>isinferredfromtypeofPair<String,Int>,whichisinferredfromtypeofparameterspassedtoPairconstructor.Wemaywonderwhathappensifinferredtypeofpairsusedtocreatemapdiffers?ThefirstpairisPair<String,Int>andsecondisPair<String,String>:
varmap=mapOf("MountEverest"to8848,"K2"to"4017")
//Inferredtype:Map<String,Any>
Intheprecedingscenario,Kotlincompilerwilltrytoinfercommontypeforallpairs.FirstparameterinbothpairsisString(MountEverest,K2),sonaturallyStringwillbeinferredhere.Secondparameterofeachpairdiffers(Intforfirstpair,Stringforsecondpair),soKotlinneedstofindtheclosestcommontype.TheAnytypeischosen,becausethisistheclosestcommontypeinupstreamtypehierarchy:
Aswecansee,typeinferencedoesagreatjobinmostcases,butwecanstillchoosetoexplicitlydefineadatatypeifwewant,forexample,wewantdifferentvariabletypes:
varage:Int=18
Whendealingwithintegers,theInttypeisalwaysadefaultchoice,butwecanstillexplicitlydefinedifferenttypes,forexample,Short,tosavesomepreciousAndroidmemory:
varage:Short=18
Ontheotherhand,ifweneedtostorelargervalues,wecandefinethetypeoftheagevariableasLong.Wecanuseexplicittypedeclarationaspreviously,oruseliteralconstant:
varage:Long=18//Explicitlydefinevariabletype
varage=18L
//Useliteralconstanttospecifyvaluetype
Thosetwodeclarationsareequal,andallofthemwillcreatevariableoftypeLong.
Fornow,weknowthattherearemorecasesincodewheretypedeclarationcanbeomittedtomakecodesyntaxmoreconcise.TherearehoweversomesituationswheretheKotlincompilerwillnotbeabletoinfertypeduetolackofinformationincontext.Forexample,simpledeclarationwithoutassignmentwillmaketypeinferenceimpossible:
valtitle//Error
Intheprecedingexample,thevariablewillbeinitializedlater,sothereisnowaytodetermineitstype.That'swhytypemustbeexplicitlyspecified.Thegeneralruleisthatiftypeofexpressionisknownforthecompiler,thentypecanbeinferred.Otherwise,itmustbeexplicitlyspecified.KotlinplugininAndroidStudiodoesagreatjobbecauseitknowsexactlywheretypecannotbeinferredandthenitishighlightingerror.Thisallowsustodisplaypropererrormessages
instantlybyIDEwhenwritingthecodewithouttheneedtocompleteapplication.
StrictnullsafetyAccordingtoAgileSoftwareAssessment(http://p3.snf.ch/Project-144126)research,missingnullcheckisthemostfrequentpatternofbugsinJavasystems.ThebiggestsourceoferrorsinJavaisNullPointerExceptions.It'ssobig,thatspeakingataconferencein2009,SirTonyHoareapologizedforinventingthenullreference,callingitabillion-dollarmistake(https://en.wikipedia.org/wiki/Tony_Hoare).
ToavoidNullPointerException,weneedtowritedefensivecodethatchecksifanobjectisnullbeforeusingit.Manymodernprogramminglanguages,includingKotlin,madestepstoconvertruntimeerrorsintocompiletimeerrorstoimproveprogramminglanguagesafeness.OneofthewaytodoitinKotlinisbyaddingnullabilitysafenessmechanismstolanguagetypesystems.ThisispossiblebecauseKotlintypesystemdistinguishesbetweenreferencesthatcanholdnull(nullablereferences)andthosethatcannot(non-nullablereferences).ThissinglefeatureofKotlinallowsustodetectmanyerrorsrelatedtoNullPointerExceptionatveryearlystagesofdevelopment.CompilertogetherwithIDEwillpreventmanyNullPointerException.Inmanycasescompilationwillfailinsteadofapplicationfailingatruntime.
StrictnullsafetyispartofKotlintypesystem.Bydefault,regulartypescannotbenull(can'tstorenullreferences),unlesstheyareexplicitlyallowed.Tostorenullreferences,wemustmarkvariableasnullable(allowittostorenullreferences)byaddingquestionmarksuffixtovariabletypedeclaration.Hereisanexample:
valage:Int=null//1,Error
valname:String?=null//2
1. Compilerwillthrowerror,becausethistypedoesnotallownull.2. Compilerwillallownullassignment,becausetypeismarkedasnullable
usingquestionmarksuffix.
Wearenotallowedtocallmethodonapotentiallynullableobject,unlessanullitycheckisperformedbeforeacall:
valname:String?=null
//...
name.toUpperCase()//error,thisreferencemaybenull
Wewilllearnhowtodealwiththeprobleminthenextsection.Everynon-nullabletypeinKotlinhasitsnullabletypeequivalent:InthasInt?,StringhasString?andsoon.ThesameruleappliesforallclassesintheAndroidframework(ViewhasView?),third-partylibraries(OkHttpClienthasOkHttpClient?),andallcustomclassesdefinedbydevelopers(MyCustomClasshasMyCustomClass?).Thismeansthateverynongenericclasscanbeusedtodefinetwokindsoftypes,nullableandnon-nullable.Anon-nullabletypeisalsoasubtypeofitsnullableequivalent.Forexample,Vehicle,aswellasbeingasubtypeofVehicle?,isalsoasubtypeofAny:
TheNothingtypeisanemptytype(uninhabitedtype),whichcan'thaveaninstance.WewilldiscussitinmoredetailsinChapter3,PlayingwithFunctions.Thistypehierarchyisthereasonwhywecanassignnon-nullobject(Vehicle)intoavariabletypedasnullable(Vehicle?),butwecannotassignanullableobject(Vehicle?)intoanon-nullvariable(Vehicle):
varnullableVehicle:Vehicle?
varvehicle:Vehicle
nullableVehicle=vehicle//1
vehicle=nullableVehicle//2,Error
1. Assignmentpossible2. ErrorbecausenullableVehiclemaybeanull
Wewilldiscusswaysofdealingwithnullabletypesinfollowingsections.Nowlet'sgetbacktotypedefinitions.Whendefininggenerictypes,therearemultiplepossibilitiesofdefiningnullability,solet'sexaminevariouscollectiontypesbycomparingdifferentdeclarationsforgenericArrayListcontainingitemsoftypeInt.Hereisatablethatispresentsthekeydifferences:
Typedeclaration Listitselfcanbenull Elementcanbenull
ArrayList<Int> No No
ArrayList<Int>? Yes No
ArrayList<Int?> No Yes
ArrayList<Int?>? Yes Yes
It'simportanttounderstanddifferentwaystospecifynulltypedeclarations,becauseKotlincompilerenforcesittoavoidNullPointerExceptions.Thismeansthatcompilerenforcesnullitycheckbeforeaccessinganyreferencethatpotentiallycanbenull.Nowlet'sexaminecommonAndroid/JavaerrorintheActivityclass'onCreatemethod:
//Java
@Override
publicvoidonCreate(BundlesavedInstanceState){
super.onCreate(savedInstanceState);
savedInstanceState.getBoolean("locked");
}
InJava,thiscodewillcompilefineandaccessingnullobjectswillresultinapplicationcrashatruntimethrowingNullPointerException.Nowlet'sexaminetheKotlinversionofthesamemethod:
overridefunonCreate(savedInstanceState:Bundle?){//1
super.onCreate(savedInstanceState)
savedInstanceState.getBoolean("key")//2Error
}
1. savedInstanceStatedefinedasnullableBundle?2. Compilerwillthrowerror
ThesavedInstanceStatetypeisaplatformtypethatcanbeinterpretedbyKotlinasnullableornon-nullable.Wewilldiscussplatformtypesinthefollowingsections,butfornowwewilldefinesavedInstanceStateasnullabletype.Wearedoingso,becauseweknowthatnullwillbepassedwhenActivityiscreatedforthefirsttime.InstanceofBundlewillonlybepassedwhenanActivityisrecreatedusingsavedinstancestate:
WewilldiscussfunctionsinChapter3,PlayingwithFunctions,butfornow,wecanalreadyseethatthesyntaxfordeclaringfunctionsinKotlinisquitesimilartoJava.
ThemostobviouswaytofixtheprecedingerrorinKotlinistocheckfornullityexactlythesamewayasinJava:
overridefunonCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
}
overridefunonCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
vallocked:Boolean
if(savedInstanceState!=null)
locked=savedInstanceState.getBoolean("locked")
else
locked=false
}
Theprecedingconstructpresentssomeboilerplatecode,becausenull-checkingisaprettycommonoperationinJavadevelopment(especiallyintheAndroidframework,wheremostelementsarenullable).Fortunately,Kotlinallowsafewsimplersolutionstodealwithnullablevariables.Thefirstoneisthesafecalloperator.
SafecallThesafecalloperatorissimplyaquestionmarkfollowedbyadot.It'simportanttounderstandthatsafecastoperatorwillalwaysreturnavalue.Iftheleft-handsideoftheoperatorisnull,thenitwillreturnnull,otherwiseitwillreturntheresultoftheright-handsideexpression:
overridefunonCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
vallocked:Boolean?=savedInstanceState?.getBoolean("locked")
}
IfsavedInstanceStateisnull,thennullwillbereturned,otherwisetheresultofevaluatingasavedInstanceState?.getBoolean("locked")expressionwillbereturned.Keepinmind,thatanullablereferencecallmayalwaysreturnsnullable,sotheresultofthewholeexpressionisnullableBoolean?.Ifwewanttomakesurewewillgetnon-nullableBoolean,wecancombinethesafecalloperatorcombinedwiththeelvisoperator,discussedinthenextsection.
Multiplecallsofthesavecalloperatorcanbechainedtogethertoavoidanestedifexpressionorcomplexconditionslikethis:
//Javaidiomatic-multiplechecks
valquiz:Quiz=Quiz()
//...
valcorrect:Boolean?
if(quiz.currentQuestion!=null){
if(quiz.currentQuestion.answer!=null){
//dosomething
}
}
//Kotlinidiomatic-multiplecallsofsavecalloperator
valquiz:Quiz=Quiz()
//...
valcorrect=quiz.currentQuestion?.answer?.correct
//InferredtypeBoolean?
Theprecedingchainworkslikethis--correctwillbeaccessedonlyiftheanswervalueisnotnullandanswerisaccessedonlyifthecurrentQuestionvalueisnotnull.Asaresult,theexpressionwillreturnthevaluereturnedbycorrectpropertyornullifanyobjectinthesafecallchainisnull.
ElvisoperatorTheelvisoperatorisrepresentedbyaquestionmarkfollowedbyacolon(?:)andhassuchsyntax:
firstoperand?:secondoperand
Theelvisoperatorworksasfollows:iffirstoperandisnotnull,thenthisoperandwillbereturned,otherwisesecondoperandwillbereturned.Theelvisoperatorallowsustowriteveryconcisecode.
Wecanapplytheelvisoperatortoourexampletoretrievethevariablelocked,whichwillbealwaysnon-nullable:
overridefunonCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
vallocked:Boolean=savedInstanceState?.
getBoolean("locked")?:false
}
Intheprecedingexample,theelvisoperatorwillreturnvalueofsavedInstanceState?.getBoolean("locked")expressionifsavedInstanceStateisnotnull,otherwiseitwillreturnfalse.Thiswaywecanmakesurethatthelockedvariable.Thankstoelvisoperatorwecandefinedefaultvalue.Alsonotethattheright-handsideexpressionisevaluatedonlyiftheleft-handsideisnull.Itisthenprovidingdefaultvaluethatwillbeusedwhentheexpressionisnullable.Gettingbacktoourquizexamplefromtheprevioussection,wecaneasilymodifythecodetoalwaysreturnanon-nullablevalue:
valcorrect=quiz.currentQuestion?.answer?.correct?:false
Astheresult,theexpressionwillreturnthevaluereturnedbythecorrectpropertyorfalseifanyobjectinthesafecallchainisnull.Thismeansthatthevaluewillalwaysbereturned,sonon-nullableBooleantypeisinferred.
TheoperatornamecomesfromthefamousAmericansinger-songwriterElvisPresley,becausehishairstyleissimilartoaquestionmark.
NotnullassertionAnothertooltodealwithnullityisthenot-nullassertionoperator.Itisrepresentedbyadoubleexclamationmark(!!).Thisoperatorexplicitlycastsnullablevariablestonon-nullablevariables.Hereisausageexample:
vary:String?="foo"
varsize:Int=y!!.length
Normally,wewouldnotbeabletoassignavaluefromanullablepropertylengthtoanon-nullablevariablesize.However,asadeveloper,wecanassurethecompilerthatthisnullablevariablewillhaveavaluehere.Ifweareright,ourapplicationwillworkcorrectly,butifwearewrong,andthevariablehasanullvalue,theapplicationwillthrowNullPointerException.Let'sexamineouractivitymethodonCreate():
overridefunonCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
vallocked:Boolean=savedInstanceState!!.getBoolean("locked")
}
Theprecedingcodewillcompile,butwillthiscodeworkcorrectly?Aswesaidbefore,whenrestoringanactivityinstance,savedInstanceStatewillbepassedtotheonCreatemethod,sothiscodewillworkwithoutexceptions.However,whencreatinganactivityinstance,thesavedInstanceStatewillbenull(thereisnopreviousinstancetorestore),soNullPointerExceptionwillbethrownatruntime.ThisbehaviorissimilartoJava,butthemaindifferenceisthatinJavaaccessingpotentiallynullableobjectswithoutanullitycheckisthedefaultbehavior,whileinKotlinwehavetoforceit;otherwise,wewillgetacompilationerror.
Thereareonlyfewcorrectpossibleapplicationsforusageofthisoperator,sowhenyouuseitorseeitincode,thinkaboutitaspotentialdangerorwarning.Itissuggestedthatnot-nullassertionshouldbeusedrarely,andinmostcasesshouldbereplacedwithsafecallorsmartcast.
Combatingnon-nullassertionsarticlepresentsfewusefulexampleswherenon-nullassertionoperatorisreplacedwithother,safeKotlinconstructsathttp://bit.ly/2xg5JXt.
Actuallyinthiscasethereisnopointofusingnot-nullassertionoperatorbecausewecansolveourprobleminsaferwayusinglet.
LetAnothertooltodealwithnullablevariablesislet.Thisisactuallynottheoperator,northelanguagespecialconstruct.ItisafunctiondefinedintheKotlinstandardlibrary.Let'sseethesyntaxofletcombinedwiththesafecalloperator:
overridefunonCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
savedInstanceState?.let{
println(it.getBoolean("isLocked"))//1
}
}
1. savedInstanceStateinsideletcanbeaccessedusingvariablenamedit.
Asmentionedbefore,theright-handsideexpressionofthesafecalloperatorwillbeonlybeevaluatediftheleft-handsideisnotnull.Inthiscase,theright-handsideisaletfunctionthattakesanotherfunction(lambda)asaparameter.CodedefinedintheblockafterletwillbeexecutedifsavedInstanceStateisnotnull.WewilllearnmoreaboutitandhowtodefinesuchfunctionslaterinChapter7,ExtensionFunctionsandProperties.
NullabilityandJavaWeknowthatKotlinrequirestoexplicitlydefinereferencesthatcanholdnullvalues.Javaontheotherhand,ismuchmorelenientaboutnullability,sowemaywonderhowKotlinhandlestypescomingfromJava(basicallythewholeAndroidSDKandlibrarieswritteninJava).Wheneverpossible,Kotlincompilerwilldeterminetypenullabilityfromthecodeandrepresenttypesasactualnullableornon-nullabletypesusingnullabilityannotations.
TheKotlincompilersupportsseveralflavorsofnullabilityannotations,including:
Android(com.android.annotationsandandroid.support.annotations)JetBrains(@Nullableand@NotNullfromtheorg.jetbrains.annotationspackage)JSR-305(Javax.annotation)
WecanfindthefulllistintheKotlincompilersourcecode(https://github.com/JetBrains/kotlin/blob/master/core/descriptor.loader.Java/src/org/jetbrains/kotlin/load/Java/JvmAnnotationNames.kt)
WehaveseenthispreviouslyinActivity'sonCreatemethod,wherethesavedInstanceStatetypewasexplicitlysettothenullabletypeBundle?:
overridefunonCreate(savedInstanceState:Bundle?){
...
}
Thereare,however,manysituationswhereitisnotpossibletodeterminevariablenullability.AllvariablescomingfromJavacanbenullexceptonesannotatedasnon-nullable..Wecouldtreatallofthemasnullableandcheckbeforeeachaccess,butthiswouldbeimpractical.Asasolutionforthisproblem,Kotlinintroducedtheconceptofplatformtypes.ThosearetypescomingfromJavatypeswithrelaxednullchecks,meaningthateachplatformtypemaybenullornot.
Althoughwecannotdeclareplatformtypesbyourselves,thisspecialsyntax
existsbecausethecompilerandAndroidStudioneedtodisplaythemsometimes.Wecanspotplatformtypesinexceptionmessagesorthemethodparameterslist.Platformtypesyntaxisjustasingleexclamationmarksuffixinavariabletypedeclaration:
View!//Viewdefinedasplatformtype
Wecouldtreateachplatformtypeasnullable,buttypenullabilityusuallydependsoncontext,sosometimeswecantreatthemasnon-nullablevariables.Thispseudocodeshowsthepossiblemeaningofplatformtype:
T!=TorT?
It'sourresponsibilityasdeveloperstodecidehowtotreatsuchtype,asnullableornon-nullable.Let'sconsidertheusageofthefindViewByIdmethod:
valtextView=findViewById(R.id.textView)
WhatwillthefindViewByIdmethodactuallyreturn?WhatistheinferredtypeofthetextViewvariable?Nullabletype(TestView)ornotnullable(TextView?)?Bydefault,theKotlincompilerknowsnothingaboutthenullabilityofthevaluereturnedbythefindViewByIdmethod.ThisiswhyinferredtypeforTextViewhasplatformtypeView!.
Thisisthekindofdeveloperresponsibilitythatwearetalkingabout.We,asdevelopers,mustdecide,becauseonlyweknowifthelayoutwillhavetextViewdefinedinallconfigurations(portrait,landscape,andsoon)oronlyinsomeofthem.IfwedefineproperviewinsidecurrentlayoutfindViewByIdmethodwillreturnreferencetothisview,andotherwiseitwillreturnnull:
valtextView=findViewById(R.id.textView)asTextView//1
valtextView=findViewById(R.id.textView)asTextView?//2
1. AssumingthattextViewispresentineverylayoutforeachconfiguration,sotextViewcanbedefinedasnon-nullable
2. AssumingthattextViewisnotpresentinalllayoutconfigurations(forexample,presentonlyinlandscape),textViewmustbedefinedasnullable,otherwisetheapplicationwillthrowaNullPointerExceptionwhentryingtoassignnulltoanon-nullablevariable(whenlayoutwithouttextViewisloaded)
CastsThecastingconceptissupportedbymanyprogramminglanguages.Basically,castingisawaytoconvertanobjectofoneparticulartypeintoanothertype.InJava,weneedtocastanobjectexplicitlybeforeaccessingitsmembersorcastitandstoreitinthevariableofthecastedtype.Kotlinsimplifiesconceptofcastingandmovesittothenextlevelbyintroducingsmartcasts.
InKotlin,wecanperformafewtypesofcasts:
Castobjectstodifferenttypesexplicitly(safecastoperator)Castobjectstodifferenttypesornullabletypestonon-nullabletypesimplicitly(smartcastmechanism)
Safe/unsafecastoperatorInstronglytypedlanguages,suchasJavaorKotlin,weneedtoconvertvaluesfromonetypetoanotherexplicitlyusingthecastoperator.Atypicalcastingoperationistakinganobjectofoneparticulartypeandturningitintoanotherobjecttypethatisitssupertype(upcasting),subtype(downcasting),orinterface.Let'sstartwithasmallremainderofcastingthatcouldbeperformedinJava:
Fragmentfragment=newProductFragment();
ProductFragmentproductFragment=(ProductFragment)fragment;
Intheprecedingexample,thereisaninstanceofProductFragmentthatisassignedtoavariablestoringFragmentdatatype.TobeabletostorethisdataintotheproductFragmentvariablethatcanstoreonlytheProductFragmentdatatype,soweneedtoperformanexplicitcast.UnlikeJava,Kotlinhasaspecialaskeywordrepresentingtheunsafecastoperatortohandlecasting:
valfragment:Fragment=ProductFragment()
valproductFragment:ProductFragment=fragmentasProductFragment
TheProductFragmentvariableisasubtypeofFragment,sotheprecedingexamplewillworkfine.TheproblemisthatcastingtoanincompatibletypewillthrowtheexceptionClassCastException.That'swhytheasoperatoriscalledanunsafecastoperator:
valfragment:String="ProductFragment"
valproductFragment:ProductFragment=fragmentas
ProductFragment
\\Exception:ClassCastException
Tofixthisproblem,wecanusethesafecastoperatoras?.Itissometimescalledthenullablecastoperator.Thisoperatortriestocastavaluetothespecifiedtype,andreturnsnullifthevaluecannotbecasted.Hereisanexample:
valfragment:String="ProductFragment"
valproductFragment:ProductFragment?=fragmentas?
ProductFragment
Notice,thatusageofthesafecastoperatorrequiresustodefinethenamevariableasnullable(ProductFragment?insteadofProductFragment).Asanalternative,wecanusetheunsafecastoperatorandnullabletypeProductFragment?,sowecansee
exactlythetypethatwearecastingto:
valfragment:String="ProductFragment"
valproductFragment:ProductFragment?=fragmentas
ProductFragment?
IfwewouldliketohaveaproductFragmentvariablethatisnon-nullable,thenwewouldhavetoassignadefaultvalueusingtheelvisoperator:
valfragment:String="ProductFragment"
valproductFragment:ProductFragment?=fragmentas?
ProductFragment?:ProductFragment()
Now,thefragmentas?ProductFragmentexpressionwillbeevaluatedwithoutasingleerror.Ifthisexpressionreturnsanon-nullablevalue(thecastcanbeperformed),thenthisvaluewillbeassignedtotheproductFragmentvariable,otherwiseadefaultvalue(thenewinstanceofProductFragment)willbeassignedtotheproductFragmentvariable.Hereisacomparisonbetweenthesetwooperators:
Unsafecast(as):ThrowsClassCastExceptionwhencastingisimpossibleSafecast(as?):Returnsnullwhencastingimpossible
Now,whenweunderstandthedifferencebetweensafecastandunsafecastoperators,wecansafelyretrieveafragmentfromthefragmentmanager:
varproductFragment:ProductFragment?=supportFragmentManager
.findFragmentById(R.id.fragment_product)as?ProductFragment
Thesafecastandunsafecastoperatorsareusedforcastingcomplexobjects.Whenworkingwithprimitivetypes,wecansimplyuseoneoftheKotlinstandardlibraryconversionmethods.MostoftheobjectsfromtheKotlinstandardlibraryhavestandardmethodsusedtosimplifycommoncastingtoothertypes.Theconventionisthatthiskindoffunctionshaveprefixto,andthenameoftheclassthatwewanttocastto.Inthelineinthisexample,theInttypeiscastedtotheStringtypeusingthetoStringmethod:
valname:String
valage:Int=12
name=age.toString();//ConvertsInttoString
Wewilldiscussprimitivetypesandtheirconversionsintheprimitivedatatypessection.
SmartcastsSmartcastingconvertsvariableofonetypetoanothertype,butasopposedtosafecasting,itisdoneimplicitly(wedon'tneedtousetheasoras?castoperator).SmartcastsworkonlywhentheKotlincompilerisabsolutelysurethatthevariablewillnotbechangedaftercheck.Thismakesthemperfectlysafeformultithreadedapplications.Generallysmartcastsareavailableforallimmutablereferences(val)andforlocalmutablereferences(var).Wehavetwokindsofsmartcasts:
TypesmartcastthatcastobjectsofonetypetoanobjectofanothertypeNullitysmartcastthatcastnullablereferencestonon-nullable
TypesmartcastsLet'srepresenttheAnimalandFishclassfromtheprevioussection:
Let'sassumewewanttocalltheisHungrymethodandwewanttocheckiftheanimalisaninstanceofFish.InJavawewouldhavetodosomethinglikethis:
\\Java
if(animalinstanceofFish){
Fishfish=(Fish)animal;
fish.isHungry();
//or
((Fish)animal).isHungry();
}
Theproblemwiththiscodeisitsredundancy.WehavetocheckifanimalinstanceisFishandthenwehavetoexplicitlycastanimaltoFishafterthischeck.Wouldn'titbeniceifcompilercouldhandlethisforus?ItturnsoutthattheKotlincompilerisreallysmartwhenitcomestocasts,soitwillhandleallthoseredundantcastsforus,usingthesmartcastsmechanism.Hereisanexampleofsmartcasting:
if(animalisFish){
animal.isHungry()
}
SmartcastinAndroidStudio
AndroidStudiowilldisplaypropererrorsifsmartcastingisnotpossible,sowewillknowexactlyifwecanuseit.AndroidStudiomarksvariableswithgreenbackgroundwhenweaccessamemberthatrequiredacast.
InKotlin,wedon'thavetoexplicitlycastananimalinstancetoaFish,becauseafterthetypecheck,Kotlincompilerwillbeabletohandlecastsimplicitly.Nowinsidetheifblock,thevariableanimaliscastedtoFish.TheresultisthenexactlythesameasinpreviousJavaexample(theJavainstanceoftheoperatoriscalledisinKotlin).ThisiswhywecansafelycalltheisHungrymethodwithoutanyexplicitcasting.Notice,thatinthiscase,thescopeofthissmartcastislimitedbytheifblock:
if(animalisFish){
animal.isHungry()//1
}
animal.isHungry()//2,Error
1. InthiscontextanimalinstanceisFish,sowecancallisHungrymethod.2. InthiscontextanimalinstanceisstillAnimal,sowecan'tcallisHungry
method.
Thereare,however,othercaseswherethesmartcastscopeislargerthanasingleblock,aslikeinthefollowingexample:
valfish:Fish?=//...
if(animal!isFish)//1
return
animal.isHungry()//1
1. Fromthispoint,animalwillbeimplicitlyconvertedtonon-nullableFish
Intheprecedingexample,thewholemethodwouldreturnfromfunctionifanimalisnotFish,sothecompilerknowsthatanimalmustbeaFishacrosstherestofthecodeblock.KotlinandJavaconditionalexpressionsareevaluatedlazily.
Itmeansthatinexpressioncondition1()&&condition2(),methodcondition2willbecalledonlywhencondition1returnstrue.Thisiswhywecanuseasmartcastedtypeintheright-handsideoftheconditionalexpression:
if(animalisFish&&animal.isHungry()){
println("Fishishungry")
}
NoticethatiftheanimalwasnotaFish,thesecondpartoftheconditionalexpressionwouldnotbeevaluatedatall.Whenitisevaluated,KotlinknowsthatanimalisaFish(smartcast).
Non-nullablesmartcastSmartcastsalsohandleothercases,includingnullitychecks.Let'sassumethatwehaveaviewvariablethatismarkedasnullable,becausewedon'tknowwhereverornotfindViewByIdwillreturnaviewornull:
valview:View?=findViewById(R.layout.activity_shop)
Wecouldusesafecalloperatortoaccessviewmethodsandproperties,butinsomecaseswemaywanttoperformmoreoperationsonthesameobject.Inthesesituationssmartcastingmaybeabettersolution:
valview:View?
if(view!=null){
view.isShown()
//viewiscastedtonon-nullableinsideifcodeblock
}
view.isShown()//error,outsideiftheblockviewisnullable
Whenperformingnullcheckslikethis,thecompilerautomaticallycastsanullableview(View?)tonon-nullable(View).ThisiswhywecancalltheisShownmethodinsidetheifblock,withoutusingasafecalloperator.Outsidetheifblock,theviewisstillnullable.
Eachsmartcastsworksonlywithread-onlyvariables,becauseread-writevariablemaychangebetweenthetimethecheckwasperformedandthetimethevariableisaccessed.
Smartcastsalsoworkwithafunction'sreturnstatements.Ifweperformnullitychecksinsidethefunctionwithareturnstatement,thenthevariablewillalsobecastedtonon-nullable:
funsetView(view:View?){
if(view==null)
return
//viewiscastedtonon-nullable
view.isShown()
}
Inthiscase,Kotlinisabsolutelysurethatthevariablevaluewillnotbenull,
becausethefunctionwouldcallreturnotherwise.FunctionswillbediscussedinmoredetailinChapter3,PlayingwithFunctions.Wecanmaketheprecedingsyntaxevensimplerbyusingelvisoperatorandperformanullitycheckinasingleline:
funverifyView(view:View?){
view?:return
//viewiscastedtonon-nullable
view.isShown()
//..
}
Insteadofjustreturningfromthefunction,wemaywanttobemoreexplicitaboutexistingproblemandthrowanexception.Thenwecanuseelvisoperatortogetherwiththeerrorthrow:
funsetView(view:View?){
view?:throwRuntimeException("Viewisempty")
//viewiscastedtonon-nullable
view.isShown()
}
Aswecansee,smartcastsareaverypowerfulmechanismthatallowsustodecreasethenumberofnullitychecks.ThisiswhyitisheavilyexploitedbyKotlin.Rememberthegeneralrule--smartcastsworkonlyifKotlinisabsolutelysurethatthevariablecannotchangeafterthecastevenbyanotherthread.
PrimitivedatatypesInKotlin,everythingisanobject(referencetype,notprimitivetype).Wedon'tfindprimitivetypes,likeoneswecanuseinJava.Thisreducescodecomplexity.Wecancallmethodsandpropertiesonanyvariable.Forexample,thisishowwecanconverttheIntvariabletoaChar:
varcode:Int=75
code.toChar()
Usually(wheneveritispossible),underthehoodtypessuchasInt,Long,orCharareoptimized(storedasprimitivetypes)butwecanstillcallmethodsonthemasonanyotherobjects.
Bydefault,JavaplatformstoresnumbersasJVMprimitivetypes,butwhenanullablenumberreference(forexample,Int?)isneededorgenericsareinvolved,Javausesboxedrepresentation.Boxingmeanswrappingaprimitivetypeintocorrespondingboxedprimitivetype.Thismeansthattheinstancebehavesasanobject.ExamplesofJavaboxedrepresentationsofprimitivetypesareintversusIntegeroralongversusLong.SinceKotliniscompiledtoJVMbytecode,thesameistruehere:
varweight:Int=12//1
varweight:Int?=null//2
1. Valueisstoredasprimitivetype2. Valueisstoredasboxedinteger(compositetype)
Thismeansthateachtimewecreateanumber(Byte,Short,Int,Long,Double,Float),orwithChar,Boolean,itwillbestoredasaprimitivetypeunlesswedeclareitasanullabletype(Byte?,Char?,Array?,andsoon);otherwise,itwillbestoredasaboxedrepresentation:
vara:Int=1//1
varb:Int?=null//2
b=12//3
1. aisnon-nullable,soitisstoredasprimitivetype2. bisnullsoitisstoredasboxedrepresentation
3. bisstillstoredasboxedrepresentationalthoughithasavalue
Generictypescannotbeparameterizedusingprimitivetypes,soboxingwillbeperformed.It'simportanttorememberthatusingboxedrepresentation(compositetype)insteadofprimaryrepresentationcanhaveperformancepenalties,becauseitwillalwayscreatememoryoverheadcomparedtoprimitivetyperepresentation.Thismaybenoticeableforlistsandarrayscontainingahugenumberofelements,sousingprimaryrepresentationmaybecrucialforapplicationperformance.Ontheotherhand,weshouldnotworryaboutthetypeofrepresentationwhenitcomestoasinglevariableorevenmultiplevariabledeclarations,evenintheAndroidworld,wherememoryislimited.
Nowlet'sdiscussthemostimportantKotlinprimitivedatatypes:numbers,characters,Booleans,andarrays.
NumbersBasicKotlindatatypesusedfornumbersareequivalentsofJavanumericprimitives:
Kotlin,however,handlesnumbersalittlebitdifferentlythanJava.Thefirstdifferenceisthattherearenoimplicitconversionsfornumbers--smallertypesarenotimplicitlyconvertedtobiggertypes:
varweight:Int=12
vartruckWeight:Long=weight//Error1
ThismeansthatwecannotassignavalueoftypeInttotheLongvariablewithoutanexplicitconversion.Aswesaid,inKotlineverythingisanobject,sowecancallthemethodandexplicitlyconvertInttypetoLongtofixtheproblem:
varweight:Int=12
vartruckWeight:Long=weight.toLong()
Atfirst,thismayseemlikeboilerplatecode,butinpracticethiswillallowustoavoidmanyerrorsrelatedtonumberconversionandsavealotofdebuggingtime.ThisisactuallyarareexamplewhereKotlinsyntaxhasmoreamountofcodethanJava.TheKotlinstandardlibrarysupportsthefollowingconversionmethodsfornumbers:
toByte():BytetoShort():ShorttoInt():InttoLong():LongtoFloat():FloattoDouble():Double
toChar():Char
Wecan,however,explicitlyspecifyanumberliteraltochangetheinferredvariabletype:
vala:Int=1
valb=a+1//InferredtypeisInt
valb=a+1L//InferredtypeisLong
TheseconddifferencebetweenKotlinandJavanumbersisthatnumberliteralsareslightlydifferentinsomecases.Therearethefollowingkindsofliteralconstantsforintegralvalues:
27//Decimalsbydefault
27L//LongsaretaggedbyauppercaseLsuffix
0x1B//Hexadecimalsaretaggedby0xprefix
0b11011//Binariesaretaggedby0bprefix
Octalliteralsarenotsupported.Kotlinalsosupportsaconventionalnotationforfloating-pointnumbers:
27.5//InferredtypeisDouble
27.5F//InferredtypeisFloat.FloataretaggedbyforF
CharCharactersinKotlinarestoredintypeChar.Inmanyways,charactersaresimilartostrings,sowewillconcentrateonthesimilaritiesanddifferences.TodefineChar,wemustuseasinglequotekindofoppositetoaStringwhereweareusingdoublequotes:
valchar='a'\\1
valstring="a"\\2
1. DefinesvariableoftypeChar2. DefinesvariableoftypeString
Inbothcharactersandstrings,specialcharacterscanbeescapedusingabackslash.Thefollowingescapesequencesaresupported:
\t:Tabulator\b:Backspace\n:Newline\r:Newline\':Quote\":Doublequote\\:Slash\$:Dollarcharacter\u:Unicodeescapesequence
Let'sdefineCharcontainingtheYinYangunicodecharacter(U+262F):
varyinYang='\u262F'
ArraysInKotlin,arraysarerepresentedbytheArrayclass.TocreateanarrayinKotlin,wecanuseanumberofKotlinstandardlibraryfunctions.ThesimplestoneisarrayOf():
valarray=arrayOf(1,2,3)//inferredtypeArray<Int>
Bydefault,thisfunctionwillcreateanarrayofboxedInt.IfwewanttohaveanarraycontainingShortorLong,thenwehavetospecifyarraytypeexplicitly:
valarray2:Array<Short>=arrayOf(1,2,3)
valarray3:Array<Long>=arrayOf(1,2,3)
Aspreviouslymentioned,usingboxedrepresentationsmaydecreaseapplicationperformance.That'swhyKotlinhasafewspecializedclassesrepresentingarraysofprimitivetypestoreduceboxingmemoryoverhead:ShortArray,IntArray,LongArray,andsoon.TheseclasseshavenoinheritancerelationtotheArrayclass,althoughtheyhavethesamesetofmethodsandproperties.Tocreateinstancesofthisclasswehavetousethecorrespondingfactoryfunction:
valarray=shortArrayOf(1,2,3)
valarray=intArrayOf(1,2,3)
valarray=longArrayOf(1,2,3)
It'simportanttonoticeandkeepinmindthissubtledifference,becausethosemethodslooksimilar,butcreatedifferenttyperepresentations:
valarray=arrayOf(1,2,3)//1
valarray=longArrayOf(1,2,3)//2
1. GenericarrayofboxedLongelements(inferredtype:Array<Long>)2. ArraycontainingprimitiveLongelements(inferredtype:LongArray)
Knowingtheexactsizeofanarraywilloftenimproveperformance,soKotlinhasanotherlibraryfunction,arrayOfNulls,thatcreatesanarrayofagivensizefilledwithnullelements:
valarray=arrayOfNulls(3)//Prints:[null,null,null]
println(array)//Prints:[null,null,null]
Wecanalsofillapredefinedsizearrayusingthefactoryfunctionthattakesthearraysizeasthefirstparameterandthelambdathatcanreturntheinitialvalueofeacharrayelementgivenitsindexasthesecondparameter:
valarray=Array(5){it*2}
println(array)//Prints:[0,2,4,8,10]
Wewilldiscusslambdas(anonymousfunctions)inmoredetailinChapter5,FunctionsasFirstClassCitizen.AccessingarrayelementsinKotlinisdonethesamewayasinJava:
valarray=arrayOf(1,2,3)
println(array[1])//Prints:2
ElementarealsoindexedthesamewayasinJava,meaningthefirstelementhasindex0,secondhasindex1,andsoon.Noteverythingworksthesameandtherearesomedifferences.MainoneisthatarraysinKotlin,unlikeinJava,arraysareinvariant.WewilldiscussvarianceisChapter6,GenericsAreYourFriends.
TheBooleantypeBooleanisalogictypethathastwopossiblevalues:trueandfalse.WecanalsousethenullableBooleantype:
valisGrowing:Boolean=true
valisGrowing:Boolean?=null
Booleantypealsosupportsstandardbuilt-inoperationsthataregenerallyavailableinmostmodernprogramminglanguages:
||:LogicalOR.Returnstruewhenanyoftwopredicatesreturntrue.&&:LogicalAND.Returnstruewhenbothpredicatesreturntrue.!:Negationoperator.Returnstrueforfalse,andfalsefortrue.
Keepinmindthatwecanonlyusenot-nullBooleanforanytypeofcondition.
LikeinJava,in||and&&,predicatesareevaluatedlazily,andonlywhenneeded(lazyconjunction).
CompositedatatypesLet'sdiscussmorecomplextypesbuiltintoKotlin.SomedatatypeshavemajorimprovementscomparedtoJava,whileothersaretotallynew.
StringsStringsinKotlinbehaveinasimilarwayasinJava,buttheyhaveafewniceimprovements.
Tostarttoaccesscharactersataspecifiedindexwecanuseindexingoperatorandaccesscharacterthesamewayweaccessarrayelements:
valstr="abcd"
println(str[1])//Prints:b
WealsohaveaccesstovariousextensionsdefinedinKotlinstandardlibrary,whichmakeworkingwithstringseasier:
valstr="abcd"
println(str.reversed())//Prints:dcba
println(str.takeLast(2))//Prints:cd
println("john@test.com".substringBefore("@"))//Prints:john
println("john@test.com".startsWith("@"))//Prints:false
ThisisexactlythesameStringclassasinJava,sothesemethodsarenotpartofStringclass.Theyweredefinedasextensions.WewilllearnmoreaboutextensionsinChapter7,ExtensionFunctionsandProperties.
ChecktheStringclassdocumentationforafulllistofthemethods(https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/).
StringtemplatesBuildingstringsisaneasyprocess,butinJavaitusuallyrequireslongconcatenationexpressions.Let'sjumpstraighttoanexample.HereisastringbuiltfrommultipleelementsimplementedinJava:
\\Java
Stringname="Eva";
intage=27;
Stringmessage="Mynameis"+name+"andIam"+age+"yearsold";
InKotlin,wecangreatlysimplifytheprocessofstringcreationbyusingstringtemplates.Insteadofusingconcatenation,wecansimplyplaceavariableinsideastringusingadollarcharactertocreateaplaceholder.Duringinterpolation,stringplaceholderswillbereplacedwiththeactualvalue.Hereisanexample:
valname="Eva"
valage=27
valmessage="Mynameis$nameandIam$ageyearsold"
println(message)
//Prints:MynameisEvaandIam27yearsold
Thisisasefficientasconcatenation,becauseunderthehoodthecompiledcodecreatesaStringBuilderandappendsallthepartstogether.Stringtemplatesarenotlimitedtosinglevariables.Theycanalsocontainwholeexpressionsbetween${,and}characters.Itcanbeafunctioncallthatwillreturnthevalueorpropertyaccessasshowninthefollowingsnippet:
valname="Eva"
valmessage="Mynamehas${name.length}characters"
println(message)//Prints:Mynamehas3characters
Thissyntaxallowsustocreatemuchcleanercodewithouttheneedtobreakthestringeachtimeavaluefromavariableorexpressionisrequiredtoconstructstrings.
RangesArangeisawaytodefineasequenceofvalues.Itisdenotedbythefirstandlastvalueinthesequence.Wecanuserangestostoreweights,temperatures,time,andage.Arangeisdefinedusingdoubledotsnotation(underthehood,arangeisusingtherangeTooperator):
valintRange=1..4//1
valcharRange='b'..'g'//2
1. InferredtypeisIntRange(equivalentofi>=1&&i<=4)2. InferredtypeisCharRange(equivalentoflettersfrom'b'to'g')
Noticethatweareusingsinglequotestodefinethecharacterrange.
TheInt,Long,andChartyperangescanbeusedtoiterateovernextvaluesinthefor...eachloop:
for(iin1..5)print(i)//Prints:1234
for(iin'b'..'g')print(i)//Prints:bcdefg
Rangescanbeusedtocheckifavalueisbiggerthanastartvalueandsmallerthananendvalue:
valweight=52
valhealthy=50..75
if(weightinhealthy)
println("$weightisin$healthyrange")
//Prints:52isin50..75range
Itcanbealsousedthiswayforothertypesofrange,suchasCharRange:
valc='k'//InferredtypeisChar
valalphabet='a'..'z'
if(cinalphabet)
println("$cischaracter")//Prints:kisacharacter
InKotlin,rangesareclosed(endinclusive).Thismeansthattherangeendingvalueisincludedintorange:
for(iin1..1)print(i)//Prints:123
Note,thatrangesinKotlinareincrementalbydefault(astepisequalto1bydefault):
for(iin5..1)print(i)//Printsnothing
Toiterateinreverseorder,wemustuseadownTofunctionthatissettingastepto-1.Likeinthisexample:
for(iin5downTo1)print(i)//Prints:54321
Wecanalsosetdifferentsteps:
for(iin3..6step2)print(i)//Prints:35
Noticethatinthe3..6range,thelastelementwasnotprinted.Thisisbecausethesteppingindexismovingtwostepsineachoftheloopiterations.Sointhefirstiterationithasavalueof3,intheseconditerationavalueof5,andfinally,inathirditerationthevaluewouldbe7,soitisignored,becauseitisoutsidetherange.
Astepdefinedbythestepfunctionmustbepositive.IfwewanttodefineanegativestepthenweshouldusethedownTofunctiontogetherwiththestepfunction:
for(iin9downTo1step3)print(i)//Prints:963
CollectionsAveryimportantaspectofprogrammingisworkingwithcollections.KotlinoffersmultiplekindsofcollectionsandmanyimprovementscomparedtoJava.WewilldiscussthissubjectinChapter7,ExtensionFunctionsandProperties.
StatementsversusexpressionsKotlinutilizesexpressionsmorewidelythanJava,soitisimportanttoknowthedifferencebetweenastatementandanexpression.Aprogramisbasicallyasequenceofstatementsandexpressions.Expressionproducesavalue,whichcanbeusedaspartofanotherexpression,variableassignment,orfunctionparameter.Anexpressionisasequenceofoneormoreoperands(datathatismanipulated)andzeroormoreoperators(atokenthatrepresentsaspecificoperation)thatcanbeevaluatedtoasinglevalue:
Let'sreviewsomeexamplesofexpressionsfromKotlin:
Expression(produceavalue) Assignedvalue Expressionoftype
a=true true Boolean
a="foo"+"bar" "foobar" String
a=min(2,3) 2 Integer
a=computePosition().getX() ValuereturnedbygetXmethod Integer
Statements,ontheotherhand,performanactionandcannotbeassignedtoavariable,becausetheysimplydon'thaveavalue.Statementscancontainlanguagekeywordsthatareusedtodefineclasses(class),interfaces(interface),variables(val,var),functions(fun),looplogic(break,continue)andsoon.Expressionscanalsobetreatedasastatementwhenthevaluereturnedbytheexpressionisignored(donotassignvaluetovariable,donotreturnitfromafunction,donotuseitaspartofotherexpressions,andsoon).
Kotlinisanexpression-orientedlanguage.ThismeansthatmanyconstructsthatarestatementsinJavaaretreatedasexpressionsinKotlin.ThefirstmajordifferenceisthefactthatJavaandKotlinhavedifferentwaysoftreatingcontrolstructures.InJavatheyaretreatedasstatementswhileinKotlinallcontrolstructuresaretreatedasexpressions,exceptforloops.ThismeansthatinKotlinwecanwriteveryconcisesyntaxusingcontrolstructures.Wewillseeexamplesinupcomingsections.
ControlflowKotlinhasmanycontrolflowelementsknownfromJava,buttheyofferalittlebitmoreflexibilityandinsomecasestheirusageissimplified.KotlinintroducesanewcontrolflowconstructknownaswhenasareplacementforJavaswitch...case.
TheifstatementAtitscore,Kotlin'sifclauseworksthesamewayasinJava:
valx=5
if(x>10){
println("greater")
}else{
println("smaller")
}
Theversionwiththeblockbodyisalsocorrectiftheblockcontainssinglestatementsorexpressions:
valx=5
if(x>10)
println("greater")
else
println("smaller")
Java,however,treatsifasastatementwhileKotlintreatsifasanexpression.Thisisthemaindifference,andthisfactallowsustousemoreconcisesyntax.Wecan,forexample,passtheresultofanifexpressiondirectlyasafunctionargument:
println(if(x>10)"greater"else"smaller")
Wecancompressourcodeintosingleline,becauseresulttheifexpression(oftypeString)isevaluatedandthenpassedtotheprintlnmethod.Whenconditionx>10istrue,thenfirstbranch(greater)willbereturnedbythisexpression,otherwisethesecondbranch(smaller)willbereturnedbythisexpression.Let'sexamineanotherexample:
valhour=10
valgreeting:String
if(hour<18){
greeting="Goodday"
}else{
greeting="Goodevening"
}
Intheprecedingexample,weareusingifasastatement.Butasweknow,ifin
Kotlinisanexpressionandtheresultoftheexpressioncanbeassignedtoavariable.Thiswaywecanassigntheresultoftheifexpressiontoagreetingvariabledirectly:
valgreeting=if(hour<18)"Goodday"else"Goodevening"
Butsometimesthereisaneedtoplacesomeothercodeinsidethebranchoftheifstatement.Wecanstilluseifasanexpression.Thenthelastlineofthematchingifbranchwillbereturnedasaresult:
valhour=10
valgreeting=if(hour<18){
//somecode
"Goodday"
}else{
//somecode
"Goodevening"
}
println(greeting)//Prints:"Goodday"
Ifweareusingifasanexpressionratherthanastatement,theexpressionisrequiredtohaveanelsebranch.TheKotlinversionisevenbetterthanJava.Sincethegreetingvariableisdefinedasnon-nullable,thecompilerwillvalidatethewholeifexpressionanditwillcheckthatallcasesarecoveredwithbranchconditions.Sinceifisanexpression,wecanuseitinsidestringtemplate:
valage=18
valmessage="Youare${if(age<18)"young"else"ofage"}person"
println(message)//Prints:Youareofageperson
TreatingifasexpressiongivesusawiderangeofpossibilitiespreviouslyunavailableinJavaworld.
ThewhenexpressionThewhenexpressioninKotlinisamultiwaybranchstatement.ThewhenexpressionisdesignedasamorepowerfulreplacementoftheJavaswitch...casestatement.Thewhenstatementoftenprovidesabetteralternativethanalargeseriesofif...elseifstatements,butitprovidesmoreconcisesyntax.Let'slookatanexample:
when(x){
1->print("x==1")
2->print("x==2")
else->println("xisneither1nor2")
}
Thewhenexpressionmatchesitsargumentagainstallbranchesoneafteranotheruntiltheconditionofsomebranchissatisfied.ThisbehaviorissimilartoJavaswitch...case,butwedonothavetowritearedundantbreakstatementaftereverybranch.
Similartotheifclause,wecanusewheneitherasastatementignoringreturnedvalueorasexpressionandassignitsvaluetoavariable.Ifwhenisusedasanexpression,thevalueofthelastlineofthesatisfiedbranchbecomesthevalueoftheoverallexpression.Ifitisusedasastatement,thevalueissimplyignored.Asusual,theelsebranchisevaluatedifnoneofthepreviousbranchessatisfythecondition:
valvehicle="Bike"
valmessage=when(vehicle){
"Car"->{
//Somecode
"Fourwheels"
}
"Bike"->{
//Somecode
"Twowheels"
}
else->{
//somecode
"Unknownnumberofwheels"
}
}
println(message)//Prints:Twowheels
Eachtimeabranchhasmorethanoneinstruction,wemustplaceitinsidethecodeblock,definedbytwobraces{...}.Ifwhenistreatedasanexpression(resultofevaluatingwhenisassignedtovariable),thelastlineofeachblockistreatedasreturnvalue.Wehaveseenthesamebehaviorwithanifexpression,sobynowweprobablyfiguredoutthatthisiscommonbehavioracrossmanyKotlinconstructsincludinglambdas,whichwillbediscussedfurtheracrossthebook.
Ifwhenisusedasanexpression,theelsebranchismandatory,unlessthecompilercanprovethatallpossiblecasesarecoveredwithbranchconditions.Wecanalsohandlemanymatchingargumentsinasinglebranchusingcommastoseparatethem:
valvehicle="Car"
when(vehicle){
"Car","Bike"->print("Vehicle")
else->print("Unidentifiedfunnyobject")
}
Anothernicefeatureofwhenistheabilitytocheckvariabletype.Wecaneasilyvalidatethatvalueisor!isofaparticulartype.Smartcastsbecomehandyagain,becausewecanaccessthemethodsandpropertiesofamatchingtypeinabranchblockwithoutanyextrachecks:
valname=when(person){
isString->person.toUpperCase()
isUser->person.name
//CodeissmartcastedtoString,sowecan
//callStringclassmethods
//...
}
Inasimilarway,wecancheckwhateverrangeorcollectioncontainsaparticularvalue.Thistimewe'lluseisand!iskeywords:
valriskAssessment=47
valrisk=when(riskAssessment){
in1..20->"negligiblerisk"
!in21..40->"minorrisk"
!in41..60->"majorrisk"
else->"undefinedrisk"
}
println(risk)//Prints:majorrisk
Actually,wecanputanykindofexpressionontheright-handsideofthewhenbranch.Itcanbeamethodcalloranyotherexpression.Considerthefollowing
examplewherethesecondwhenexpressionisusedfortheelsestatement:
valriskAssessment=80
valhandleStrategy="Warn"
valrisk=when(riskAssessment){
in1..20->print("negligiblerisk")
!in21..40->print("minorrisk")
!in41..60->print("majorrisk")
else->when(handleStrategy){
"Warn"->"Riskassessmentwarning"
"Ignore"->"Riskignored"
else->"Unknownrisk!"
}
}
println(risk)//Prints:Riskassessmentwarning
Aswecansee,whenisaverypowerfulconstructallowingmorecontrolthanJavaswitch,butitisevenmorepowerfulbecauseitisnotlimitedonlytocheckingvaluesforequality.Inaway,itcanevenbeusedasareplacementforanif...elseifchain.Ifnoargumentissuppliedtothewhenexpression,thebranchconditionsbehaveasBooleanexpressions,andabranchisexecutedwhenitsconditionistrue:
privatefungetPasswordErrorId(password:String)=when{
password.isEmpty()->R.string.error_field_required
passwordInvalid(password)->R.string.error_invalid_password
else->null
}
Allthepresentedexamplesrequireanelsebranch.Eachtimewhenallthepossiblecasesarecovered,wecanomitanelsebranch(exhaustivewhen).Let'slookatthesimplestexamplewithBoolean:
vallarge:Boolean=true
when(large){
true->println("Big")
false->println("Big")
}
Compilercanverifythatallpossiblevaluesarehandled,sothereisnoneedtospecifyanelsebranch.ThesamelogicappliestoenumsandsealedclassesthatwillbediscussedinChapter4,ClassesandObjects.
ChecksareperformedbytheKotlincompiler,sowehavecertaintythatanycasewillnotbemissed.ThisreducesthepossibilityofacommonJavabugwherethedeveloperforgetstohandleallthecasesinsidetheswitchstatement(although
polymorphismisusuallyabettersolution).
LoopsLoopisacontrolstructurethatrepeatsthesamesetofinstructionsuntilaterminationconditionismet.InKotlin,loopscaniteratethroughanythingthatprovidesiterator.Iteratorisaninterfacethathastwomethods:hasNextandnext.Itknowshowtoiterateoveracollection,range,string,oranyentitythatcanberepresentedasasequenceofelements.
Toiteratethroughsomething,wehavetosupplyaniterator()method.AsStringdoesn'thaveone,soinKotlinitisdefinedasanextensionfunction.ExtensionswillbecoveredinChapter7,ExtensionFunctionsandProperties.
Kotlinprovidesthreekindsofloops:for,while,anddo...while.Allofthemworkthesameasinotherprogramminglanguages,sowewilldiscussthembriefly.
TheforloopTheclassicJavaforloop,whereweneedtodefinetheiteratorexplicitly,isnotpresentinKotlin.HereisanexampleofthiskindofloopinJava:
//Java
Stringstr="FooBar";
for(inti=0;i<str.length();i++)
System.out.println(str.charAt(i));
Toiteratethroughacollectionofitemsfromstarttofinish,wecansimplyusetheforloopinstead:
vararray=arrayOf(1,2,3)
for(iteminarray){
print(item)
}
Itcanalsobedefinedwithoutablockbody:
for(iteminarray)
print(item)
Ifacollectionisagenericcollection,thenitemwillbesmartcastedtotypecorrespondingtoagenericcollectiontype.Inotherwords,ifacollectioncontainselementsoftypeInttheitemwillbesmartcasedtoInt:
vararray=arrayOf(1,2,3)
for(iteminarray)
print(item)//itemisInt
Wecanalsoiteratethroughthecollectionusingitsindex:
for(iinarray.indices)
print(array[i])
Thearray.indicesparamreturnsIntRangewithallindexes.Itistheequivalentof(1..array.length-1).ThereisalsoanalternativewithIndexlibrarymethodthatreturnsalistoftheIndexedValueproperty,whichcontainsanindexandvalue.Thiscanbedestructedintotheseelementsthisway:
for((index,value)inarray.withIndex()){
println("Elementat$indexis$value")
}
Theconstruct(index,value)isknownasadestructivedeclarationandwewilldiscussitinChapter4,ClassesandObjects.
ThewhileloopThewhilelooprepeatsablock,whileitsconditionalexpressionreturnstrue:
while(condition){
//code
}
Thereisalsoado...whileloopthatrepeatsblocksaslongasaconditionalexpressionisreturningtrue:
do{
//code
}while(condition)
Kotlin,opposedtoJava,canusevariablesdeclaredinsidethedo...whileloopascondition.
do{
varfound=false
//..
}while(found)
Themaindifferencebetweenbothwhileanddo...whileloopsiswhenaconditionalexpressionisevaluated.Awhileloopischeckingtheconditionbeforecodeexecutionandifitisnottruethenthecodewon'tbeexecuted.Ontheotherhand,ado...whileloopfirstexecutesthebodyoftheloop,andthenevaluatestheconditionalexpression,sothebodywillalwaysexecuteatleastonce.Ifthisexpressionistrue,theloopwillrepeat.Otherwise,theloopterminates.
OtheriterationsThereotherwaystoiterateovercollectionsusingbuilt-instandardlibraryfunctions,suchasforEach.WewillcovertheminChapter7,ExtensionFunctionsandProperties.
BreakandcontinueAllloopsinKotlinsupportclassicbreakandcontinuestatements.Thecontinuestatementproceedstothenextiterationofthatloopwhilebreakstopstheexecutionofthemostinnerenclosingloop:
valrange=1..6
for(iinrange){
print("$i")
}
//prints:123456
Nowlet'saddaconditionandbreaktheiterationwhenthisconditionistrue:
valrange=1..6
for(iinrange){
print("$i")
if(i==3)
break
}
//prints:123
Thebreakandcontinuestatementsareespeciallyusefulwhendealingwithnestedloops.TheymaysimplifyourcontrolflowandsignificantlydecreasetheamountofperformedworktosavepricelessAndroidresources.Let'sperformanestediterationandbreaktheouterloop:
valintRange=1..6
valcharRange='A'..'B'
for(valueinintRange){
if(value==3)
break
println("Outerloop:$value")
for(charincharRange){
println("\tInnerloop:$char")
}
}
//prints
Outerloop:1
Innerloop:A
Innerloop:B
Outerloop:2
Innerloop:A
Innerloop:B
Weusedabreakstatementtoterminatetheouterloopatthebeginningofthethirditeration,sothenestedloopwasalsoterminated.Noticetheusageofthe\tescapedsequencethataddsindentsontheconsole.Wecanalsoutilizethecontinuestatementtoskipthecurrentiterationoftheloop:
valintRange=1..5
for(valueinintRange){
if(value==3)
continue
println("Outerloop:$value")
for(charincharRange){
println("\tInnerloop:$char")
}
}
//prints
Outerloop:1
Innerloop:A
Innerloop:B
Outerloop:2
Innerloop:A
Innerloop:B
Outerloop:4
Innerloop:A
Innerloop:B
Outerloop:5
Innerloop:A
Innerloop:B
Weskiptheiterationoftheouterloopwhenthecurrentvalueequalsto3.
Bothcontinueandbreakstatementsperformcorrespondingoperationsontheenclosingloop.Thereare,however,timeswhenwewanttoterminateorskipiterationofoneloopfromwithinanother;forexample,terminateanouterloopiterationfromwithinaninnerloop:
for(valueinintRange){
for(charincharRange){
//Howcanwebreakouterloophere?
}
}
Fortunately,bothacontinuestatementandbreakstatementhavetwoforms--labeledandunlabeled.Wealreadysawunlabeled,nowwewillneedlabeledtosolveourproblem.Hereisanexampleofhowalabeledbreakmightbeused:
valcharRange='A'..'B'
valintRange=1..6
outer@for(valueinintRange){
println("Outerloop:$value")
for(charincharRange){
if(char=='B')
break@outer
println("\tInnerloop:$char")
}
}
//prints
Outerloop:1
Innerloop:A
The@outeristhelabelname.Byconvention,thelabelnamealwaysstartswith@followedbylabelname.Labelisplacedbeforetheloop.Labelingtheloopallowsustousequalifiedbreak(break@outer),whichisawaytostopexecutionofaloopthatisreferencedbythislabel.Theprecedingqualifiedbreak(breakwithlabel)jumpstotheexecutionpointrightaftertheloopmarkedwiththatlabel.
Placingthereturnstatementwillbreakalltheloopsandreturnfromenclosingananonymousornamedfunction:
fundoSth(){
valcharRange='A'..'B'
valintRange=1..6
for(valueinintRange){
println("Outerloop:$value")
for(charincharRange){
println("\tInnerloop:$char")
return
}
}
}
//usage
println("Beforemethodcall")
doSth()
println("Aftermethodcall")
//prints
Outerloop:1
Innerloop:A
Afterthemethodcall:
Outerloop:1
Innerloop:A
ExceptionsMostJavaprogrammingguidelines,includingthebookEffectiveJava,promotetheconceptofvaliditychecks.Thismeansthatweshouldalwaysverifyargumentsorthestateoftheobjectandthrowanexceptionifavaliditycheckfails.Javaexceptionsystemshavetwokindsofexceptions:checkedexceptionsanduncheckedexceptions.
Uncheckedexceptionmeansthatthedeveloperisnotforcedtocatchexceptionsbyusingatry...catchblock.Bydefault,exceptionsgoallthewayupthecallstack,sowemakedecisionswheretocatchthem.Ifweforgettocatchthem,theywillgoallthewayupthecallstackandstopthreadexecutionwithapropermessage(thustheyremindus):
Javahasareallystrongexceptionsystem,whichinmanycasesforcesdeveloperstoexplicitlymarkeachfunctionthatmaythrowanexceptionandexplicitlycatcheachexceptionbysurroundingthembytry...catchblocks(checkedexceptions).Thisworksgreatforverysmallprojects,butinreallarge-scaleapplicationsthisveryoftenleadstothefollowing,verbosecode:
//Java
try{
doSomething()
}catch(IOExceptione){
//Mustbesafe
}
Insteadofpassingtheexceptionupinthecallstack,itisignoredbyprovidinganemptycatchblock,soitwon'tbehandledproperlyanditwillvanish.Thiskindofcodemaymaskcriticalexceptionsandgiveafalsesenseofsecurityandleadtounexpectedproblemsanddifficulttofindbugs.
BeforewediscusshowexceptionhandlingisdoneinKotlin,let'scomparebothtypesofexceptions:
Code Checkedexceptions Uncheckedexceptions
Functiondeclaration
Wehavetospecifywhatexceptionscanbethrownbyfunctions.
Functiondeclarationdoesnotcontaininformationaboutallthrownexceptions.
Exceptionhandling
Functionthatthrowsexceptionmusttobesurroundedbyatry...catchblock.
Wecancatchexceptionanddosomethingifwewant,butwearen'tforcedtodothis.Exceptiongoesupinthecallstack.
ThebiggestdifferencebetweenKotlinandJavaexceptionsystemsisthatinKotlinallexceptionsareunchecked.Thismeansweneverhavetosurroundamethodwithtry...catchblockevenifthisisaJavamethodthatmaythrowacachedexception.Wecanstilldoit,butwearenotforcedto:
funfoo(){
throwIOException()
}
funbar(){
foo()//noneedtosurroundmethodwithtry-catchblock
}
Thisapproachremovescodeverbosityandimprovessafetybecausewedon'tneedtointroduceemptycatchblocks.
Thetry...catchblockKotlintry...catchblockistheequivalentoftheJavatry...catchblock.Let'slookatquickexample:
funsendFormData(user:User?,data:Data?){//1
user?:throwNullPointerException("Usercannotbenull")
//2
data?:throwNullPointerException("Datacannotbenull")
//dosomething
}
funonSendDataClicked(){
try{//3
sendFormData(user,data)
}catch(e:AssertionError){//4
//handleerror
}finally{//5
//optionalfinallyblock
}
}
1. ExceptionsarenotspecifiedonfunctionsignaturelikeinJava.2. WecheckvalidityofdataandthrowNullPointerException(noticethatnonew
keywordisrequiredwhencreatinganobjectinstance).3. Thetry...catchblockissimilarconstructfromJava.4. Handleonlythisspecificexceptions(AssertionErrorexception).5. Thefinallyblockisalwaysexecuted.
Theremaybezeroormorecatchblocksandfinallyblockmaybeomitted.However,atleastonecatchorfinallyblockshouldbepresent.
InKotlin,exceptionhandlingtryisanexpression,soitcanreturnavalueandwecanassignitsvaluetoavariable.Theactualassignedvalueisthelastexpressionoftheexecutedblock.Let'scheckifaparticularAndroidapplicationisinstalledonthedevice:
valresult=try{//1
context.packageManager.getPackageInfo("com.text.app",0)//2
true
}catch(ex:PackageManager.NameNotFoundException){//3
false
}
1. Thetry...catchblockisreturningvaluethatisreturnedbyasingleexpressionfunction.
2. Ifanapplicationisinstalled,thegetPackageInfomethodwillreturnavalue(thisvalueisignored)andthenextlinecontainingtrueexpressionwillbeexecuted.Thisisthelastoperationperformedbyatryblock,soitsvaluewillbeassignedtoavariable(true).
Ifanappisnotinstalled,getPackageInfowillthrowPackageManager.NameNotFoundExceptionandthecatchblockwillbeexecuted.Thelastlineofthecatchblockcontainsafalseexpression,soitsvaluewillbeassignedtoavariable.
Compile-timeconstantsSincethevalvariableisreadonly,inmostcaseswecouldtreatitasaconstant.Weneedtobeawarethatitsinitializationmaybedelayed,sothismeansthattherearescenarioswherethevalvariablemaynotbeinitializedatcompiletime,forexample,assigningtheresultofthemethodcalltoavalue:
valfruit:String=getName()
Thisvaluewillbeassignedatruntime.Thereare,however,situationswhereweneedtoknowthevalueatcompiletime.Theexactvalueisrequiredwhenwewanttopassparameterstoannotations.Annotationsareprocessedbyanannotationprocessorthatrunslongbeforetheapplicationisstarted:
Tomakeabsolutelysurethatthevalueisknownatcompiletime(andthuscanbeprocessedbyanannotationprocessor),weneedtomarkitwithaconstmodifier.Let'sdefineacustomannotationMyLoggerwithasingleparameterdefiningmaximumlogentriesandannotateaTestclasswithit:
constvalMAX_LOG_ENTRIES=100
@MyLogger(MAX_LOG_ENTRIES)
//valueavailableatcompiletime
classTest{}
Therearecouplelimitationsregardingusageofconstthatwemustbeawareof.ThefirstlimitationisthatitmustbeinitializedwithvaluesofprimitivetypesorStringtype.Thesecondlimitationisthatitmustbedeclaredatthetoplevelorasamemberofanobject.WewilldiscussobjectsinChapter4,ClassesandObjects.Thethirdlimitationisthattheycannothaveacustomgetter.
DelegatesKotlinprovidesfirst-classsupportfordelegation.ItisveryusefulimprovementcomparingtoJava.Iffact,therearemanyapplicationsfordelegatesinAndroiddevelopment,sowehavedecidedtospareawholechapteronthissubject(Chapter8,Delegates).
SummaryInthischapter,wehavediscussedthedifferencesbetweenvariables,values,andconstsanddiscussedbasicKotlindatatypesincludingranges.WealsolookedintoaKotlintypesystemthatenforcesstrictnullsafetyandwaystodealwithnullablereferencesusingvariousoperatorsandsmartcasts.WeknowthatwecanwritemoreconcisecodebytakingadvantageofusingtypeinferenceandvariouscontrolstructuresthatinKotlinaretreatedasexpressions.Finally,wediscussedwaysofexceptionhandling.
Inthenextchapter,wewilllearnaboutfunctionsandpresentdifferentwaysofdefiningthem.Wewillcoverconceptssuchassingle-expressionfunctions,defaultargumentsandnamedargumentsyntax,anddiscussvariousmodifiers.
PlayingwithFunctionsInpreviouschapters,we'veseenKotlinvariables,typesystems,andcontrolstructures.Buttocreateapplications,weneedbuildingblocksthatallowustomakestructures.InJava,theclassisthebuildingblockofthecode.Kotlin,ontheotherhand,supportsfunctionalprogramming;therefore,itmakesitpossibletocreatewholeprogramsorlibrarieswithoutanyclasses.FunctionisthemostbasicbuildingblockinKotlin.ThischapterintroducesfunctionsinKotlin,togetherwithdifferentfunctionfeaturesandtypes.
Inthischapter,wewillcoverthefollowingtopics:
BasicfunctionusageinKotlinUnitreturntypeThevarargparameterSingle-expressionfunctionsTail-recursivefunctionsDefaultargumentvaluesNamedargumentsyntaxTop-levelfunctionsLocalfunctionsNothingreturntype
BasicfunctiondeclarationandusagesThemostcommonfirstprogramthatprogrammerswritetotestsomeprogramminglanguageistheHello,World!program.ItisafullprogramthatisjustdisplayingHello,World!textontheconsole.Wearealsogoingtostartwiththisprogram,becauseinKotlinitisbasedonafunctionandonlyonafunction(noclassisneeded).SotheKotlinHello,World!programlooksasfollows:
//SomeFile.kt
funmain(args:Array<String>){//1
println("Hello,World!")//2,Prints:Hello,World!
}
1. Afunctiondefinessingleparameterargs,whichcontainsanarrayofallargumentsusedtoruntheprogram(fromthecommandline).Itisdefinedasnon-nullable,becauseanemptyarrayispassedtoamethodwhentheprogramisstartedwithoutanyarguments.
2. TheprintlnfunctionisaKotlinfunctiondefinedintheKotlinstandardlibrarythatisequivalentoftheJavafunctionSystem.out.println.
ThisprogramtellsusalotaboutKotlin.Itshowshowfunctionlookslikeandthatwecandefinefunctionwithoutanyclass.First,let'sanalyzethestructureofthefunction.Itstartswiththefunkeyword,andthencomesthenameofthefunction,parametersinthebracket,andthefunctionbody.Hereisanotherexampleofasimplefunction,butthisoneisreturningavalue:
fundouble(i:Int):Int{
return2*i
}
Goodtoknowframe
Thereismuchconfusionaroundthedifferencebetweenmethodsandfunction.Commondefinitionsareasfollows:
Afunctionisapieceofcodethatiscalledbyname.Themethodisafunctionassociatedwithaninstanceofclass(object).Sometimesitiscalledmemberfunction.
Soinsimplerwords,functionsinsideclassesarecalledmethods.InJava,thereareofficiallyonlymethods,butacademicenvironmentsareoftenarguingthatstaticJavamethodsareinfactfunctions.InKotlinwecandefinefunctionsthatarenotassociatedwithanyobject.
SyntaxtocallafunctionisthesameinKotlinasinJava,andmostmodernprogramminglanguages:
vala=double(5)
Wecallthedoublefunctionandassignavaluereturnedbyittoavariable.Let'sdiscussthedetailsofparametersandreturntypesofKotlinfunctions.
ParametersParametersinKotlinfunctionsaredeclaredusingthePascalnotation,andthetypeofeachparametermustbeexplicitlyspecified.Allparametersaredefinedasaread-onlyvariable.Thereisnowaytomakeparametersmutable,becausesuchbehavioriserror-proneandinJavaitwasoftenabusedbytheprogrammers.Ifthereisaneedforthat,thenwecanexplicitlyshadowparametersbydeclaringlocalvariableswiththesamename:
funfindDuplicates(list:List<Int>):Set<Int>{
varlist=list.sorted()
//...
}
Thisispossible,butitistreatedasbadpractice,soawarningwillbedisplayed.Abetterapproachistonameparametersbydatatheyprovideandvariablesbythepurposetheyserve.Thesenamesshouldbethendifferentinmostcases.
Parametersversusarguments
Intheprogrammingcommunity,argumentsandparametersareoftenthoughtobethesamething.Thesewordscannotbeusedinterchangeablybecausetheyhavedifferentmeanings.Anargumentisanactualvaluethatispassedtothefunctionwhenafunctioniscalled.Parameterreferstothevariablesdeclaredinsidefunctiondeclaration.Considerthefollowingexample:
funprintSum(a1:Int,a2:Int){//1.
print(a1+a2)
}
add(3,5)//2.
1-a1anda2areparameters
2-3and5arearguments
AswithJava,functionsinKotlincancontainmultipleparameters:
funprintSum(a:Int,b:Int){
valsum=a+b
print(sum)
}
Argumentsprovidedtofunctionscanbesubtypesofthetypespecifiedinparameterdeclaration.Asweknow,inKotlin,thesupertypeofallthenon-nullabletypesisAny,soweneedtouseit,ifwewanttoacceptalltypes:
funpresentGently(v:Any){
println("Hello.Iwouldliketopresentyou:$v")
}
presentGently("Duck")
//Hello.Iwouldliketopresentyou:Duck
presentGently(42)
//Hello.Iwouldliketopresentyou:42
Toallownullonarguments,thetypeneedstobespecifiedasnullable.NotethatAny?issupertypeofallnullableandnon-nullabletypes,sowecanpassobjectsofanytypeasarguments:
funpresentGently(v:Any?){
println("Hello.Iwouldliketopresentyou:$v")
}
presentGently(null)
//Prints:Hello.Iwouldliketopresentyou:null
presentGently(1)
//Prints:Hello.Iwouldliketopresentyou:1
presentGently("Str")
//Prints:Hello.Iwouldliketopresentyou:Str
ReturningfunctionsSofar,mostofthefunctionsweredefinedlikeprocedures(functionsthatdoesnotreturnanyvalues).Butinfact,therearenoproceduresinKotlinandallfunctionsreturnsomevalue.Whenitisnotspecified,thedefaultreturnvalueistheUnitinstance.Wecansetitexplicitlyfordemonstrationpurposes:
funprintSum(a:Int,b:Int):Unit{//1
valsum=a+b
print(sum)
}
1. UnlikeinJava,wearedefiningthereturntypeafterfunctionnameandparameters.
TheUnitobjectistheequivalentofJava'svoid,butitcanbetreatedasanyotherobject.Sowecanstoreitinvariable:
valp=printSum(1,2)
println(pisUnit)//Prints:true
Ofcourse,KotlincodingconventionsclaimsthatwhenafunctionisreturningUnitthenthetypedefinitionshouldbeomitted.Thiswaycodeismorereadableandsimplertounderstand:
funprintSum(a:Int,b:Int){
valsum=a+b
print(sum)
}
GoodtoknowframeUnitisasingleton,whatmeansthatthereisonlyoneinstanceofit.Soallthreeconditionsaretrue:
println(pisUnit)//Print:true
println(p==Unit)//Print:true
println(p===Unit)//Print:true
SingletonpatternishighlysupportedinKotlinanditwillbemorethoroughlycoveredinChapter4,Classesandobjects.
ToreturnoutputfromfunctionswithUnitreturntype,wecansimplyuseareturn
statementwithoutanyvalue:
funprintSum(a:Int,b:Int){//1
if(a<0||b<0){
return//2
}
valsum=a+b
print(sum)
//3
}
1. Thereisnoreturntypespecified,soreturntypeisimplicitlysettoUnit.
2. Wecanjustusereturnwithoutanyvalue.3. WhenafunctionreturnsUnit,thenreturncallisoptional.Wedon'thaveto
useit.
WecouldalsousereturnUnit,butitshouldnotbeusedbecausethatwouldbemisleadingandlessreadable.
Whenwespecifythereturntype,otherthantheUnit,thenwealwaysneedtoreturnthevalueexplicitly:
funsumPositive(a:Int,b:Int):Int{
if(a>0&&b>0){
returna+b
}
//Error,1
}
1. Functionwillnotcompileit,becausenoreturnvaluewasspecified,theifconditionisnotfulfilled.
Theproblemcanbefixedbyaddingasecondreturnstatement:
funsumPositive(a:Int,b:Int):Int{
if(a>=0&&b>=0){
returna+b
}
return0
}
VarargparameterSometimes,thenumberofparametersisnotknowninadvance.Insuchcaseswecanaddavarargmodifiertoaparameter.Itallowsthefunctiontoacceptanynumberofarguments.Hereisanexample,wherethefunctionisprintingthesumofmultipleintegers:
funprintSum(varargnumbers:Int){
valsum=numbers.sum()
print(sum)
}
printSum(1,2,3,4,5)//Prints:15
printSum()//Prints:0
Argumentswillbeaccessibleinsidethemethodasanarraythatholdsalltheprovidedvalues.Thetypeofthearraywillcorrespondtoavarargparametertype.Normallywewouldexpectittobeagenericarrayholdingaspecifiedtype(Array<T>),butasweknow,KotlinhasanoptimizedtypeforarrayofIntcalledIntArray,sothistypewillbeused.Here,forexample,isthetypeofthevarargparameterwiththetypeString:
funprintAll(varargtexts:String){
//InferredtypeoftextsisArray<String>
valallTexts=texts.joinToString(",")
println("Textsare$allTexts")
}
printAll("A","B","C")//Prints:TextsareA,B,C
Notethatwearestillabletospecifymoreparametersbeforeorafterthevarargparameter,aslongasitisclearwhichargumentisdirectedtowhichparameter:
funprintAll(prefix:String,postfix:String,varargtexts:String)
{
valallTexts=texts.joinToString(",")
println("$prefix$allTexts$postfix")
}
printAll("Alltexts:","!")//Prints:Alltexts:!
printAll("Alltexts:","!","Hello","World")
//Prints:Alltexts:Hello,World!
Additionally,argumentsprovidedtovarargparameterscanbesubtypesofthespecifiedtype:
funprintAll(varargtexts:Any){
valallTexts=texts.joinToString(",")//1
println(allTexts)
}
//Usage
printAll("A",1,'c')//Prints:A,1,c
1. ThejoinToStringfunctioncanbeinvokedonlists.Itisjoiningelementsintoasinglestring.Onthefirstargumentthereisaseparatorspecified.
Onelimitationwithvarargusageisthatthereisonlyonevarargparameterallowedperfunctiondeclaration.
Whenwecallvarargparameters,wecanpassargumentvaluesone-by-one,butwecanalsopassanarrayofvalues.Thiscanbedoneusingthespreadoperator(*prefixingarray),asinthefollowingexample:
valtexts=arrayOf("B","C","D")
printAll(*texts)//Prints:Textsare:B,C,D
printAll("A",*texts,"E")//Prints:Textsare:A,B,C,D,E
Single-expressionfunctionsDuringtypicalprogramming,manyfunctionscontainonlyoneexpression.Hereisanexampleofthiskindoffunction:
funsquare(x:Int):Int{
returnx*x
}
Oranotherone,whichcanbeoftenfoundinAndroidprojects,isapatternusedinActivity,todefinemethodsthatarejustgettingtextfromsomevieworprovidingsomeotherdatafromtheviewtoallowapresentertogetthem:
fungetEmail():String{
returnemailView.text.toString()
}
Bothfunctionsaredefinedtoreturnresultsofasingleexpression.Inthefirstexample,itistheresultofx*xmultiplication,andinthesecondoneitistheresultoftheexpressionemailView.text.toString().ThesekindsoffunctionsareusedallaroundAndroidprojects.Herearesomecommonusecases:
Extractingsomesmalloperations(likeintheprecedingsquarefunction)UsingpolymorphismtoprovidevaluesspecifictoaclassFunctionsthatareonlycreatingsomeobjectFunctionsthatarepassingdatabetweenarchitecturelayers(likeintheprecedingexample,Activityispassingdatafromtheviewtothepresenter)Functionalprogrammingstylefunctionsthatarebasedonrecurrence
Suchfunctionsareoftenused,soKotlinhasanotationforthiskindofthem.Whenafunctionreturnsasingleexpression,thencurlybracesandbodyofthefunctioncanbeomitted.Wespecifyexpressiondirectlyusingtheequalitycharacter.Functionsdefinedthiswayarecalledsingle-expressionfunctions.Let'supdateoursquarefunction,anddefineitasasingle-expressionfunction:
Aswecansee,single-expressionfunctionshaveexpressionbodyinsteadofblockbody.Thisnotationisshorter,butwholebodyneedstobejustasingleexpression.
Insingle-expressionfunctions,declaringthereturntypeisoptional,becauseitcanbeinferredbythecompilerfromthetypeofexpression.Thisiswhywecansimplifythesquarefunction,anddefineitthisway:
funsquare(x:Int)=x*x
TherearemanyplacesinsideAndroidapplicationswherewecanutilizesingleexpressionfunctions.Let'sconsidertheRecyclerViewadapterthatisprovidingthelayoutIDandcreatingViewHolder:
classAddressAdapter:ItemAdapter<AddressAdapter.ViewHolder>(){
overridefungetLayoutId()=R.layout.choose_address_view
overridefunonCreateViewHolder(itemView:View)=ViewHolder(itemView)
//Restofmethods
}
Inthefollowingexample,weachievehighreadabilitythankstoasingleexpressionfunction.Singleexpressionfunctionsarealsoverypopularinthefunctionalworld.Theexamplewillbedescribedlater,inthesectionabouttail-recursivefunctions.Singleexpressionfunctionnotationalsopairswellwiththewhenstructure.Hereisanexampleoftheirconnection,usedtogetspecific
datafromanobjectaccordingtoakey(usecasefrombigKotlinproject):
funvalueFromBooking(key:String,booking:Booking?)=when(key){
//1
"patient.nin"->booking?.patient?.nin
"patient.email"->booking?.patient?.email
"patient.phone"->booking?.patient?.phone
"comment"->booking?.comment
else->null
}
1. Wedon'tneedatype,becauseitisinferredfromthewhenexpression.
AnothercommonAndroidexampleisthatwecancombinewhenexpressionswithactivitymethodonOptionsItemSelectedthathandlestopbarmenuclicks:
overridefunonOptionsItemSelected(item:MenuItem):Boolean=when
{
item.itemId==android.R.id.home->{
onBackPressed()
true
}
else->super.onOptionsItemSelected(item)
}
Anotherexamplewherethesyntaxofthesingle-expressionfunctionisusefuliswhenwechainmultipleoperationsonasingleobject:
funtextFormatted(text:String,name:String)=text
.trim()
.capitalize()
.replace("{name}",name)
valformatted=textFormatted("hello,{name}","Marcin")
println(formatted)//Hello,Marcin
Aswecansee,singleexpressionfunctionscanmakeourcodemoreconciseandimprovereadability.Single-expressionfunctionsarecommonlyusedinKotlinAndroidprojectsandtheyarereallypopularforfunctionalprogramming.
Imperativeversusdeclarativeprogramming
Imperativeprogramming:Thisprogrammingparadigmdescribestheexactsequenceofstepsrequiredtoperformanoperation.Itismostintuitiveformostprogrammers.
Declarativeprogramming:Thisprogrammingparadigmdescribesadesiredresult,butnotnecessarilystepstoachieveit
(implementationofbehavior).Thismeansthatprogrammingisdonewithexpressionsordeclarationsinsteadofstatements.Bothfunctionalandlogicprogrammingarecharacterizedasdeclarativeprogrammingstyle.Declarativeprogrammingisoftenshorterandmorereadablethanimperative.
Tail-recursivefunctionsRecursivefunctionsarefunctionsthatarecallingthemselves.Let'sseeanexampleofrecursivefunction,getState:
fungetState(state:State,n:Int):State=
if(n<=0)state//1
elsegetState(nextState(state),n-1)
Theyareanimportantpartoffunctionalprogrammingstyle,buttheproblemisthateachrecursivefunctioncallneedstokeepthereturnaddressofthepreviousfunctiononthestack.Whenanapplicationrecursetoodeeply(therearetoomanyfunctionsonthestack),StackOverflowErroristhrown.Thislimitationpresentsaveryseriousproblemforrecurrenceusage.
Aclassicsolutionforthisproblemwastouseiterationinsteadofrecurrence,butthisapproachislessexpressive:
fungetState(state:State,n:Int):State{
varstate=state
for(iin1..n){
state=state.nextState()
}
returnstate
}
Apropersolutionforthisproblemisusageofthetail-recursivefunctionsupportedbymodernlanguagessuchasKotlin.Tail-recursivefunctionisaspecialkindofrecursivefunction,wherethefunctioniscallingitselfasthelastoperationitperforms(inotherwords:recursiontakesplaceinlastoperationofafunction).ThisallowsustooptimizerecursivecallsbycompilerandperformrecursiveoperationsinamoreefficientwaywithoutworryingaboutpotentialStackOverflowError.Tomakeafunctiontail-recursive,weneedtomarkitwithatailrecmodifier:
tailrecfungetState(state:State,n:Int):State=
if(n<=0)state
elsegetState(state.nextState(),n-1)
Tocheckouthowitisworking,let'scompilethiscodeanddecompiletoJava.Hereiswhatcanbefoundthen(codeaftersimplification):
publicstaticfinalStategetState(@NotNullStatestate,intn)
{
while(true){
if(n<=0){
returnstate;
}
state=state.nextState();
n=n-1;
}
}
Implementationisbasedoniteration,sothereisnowaythatstackoverflowerrormighthappen.Tomakethetailrecmodifierwork,therearesomerequirementstobemet:
ThefunctionmustcallitselfonlyasthelastoperationitperformsItcannotbeusedwithintry/catch/finallyblocksAtthetimeofwriting,itwasallowedonlyinKotlincompiledtoJVM
DifferentwaysofcallingafunctionSometimesweneedtocallafunctionandprovideonlyselectedarguments.InJava,wecouldcreatemultipleoverloadsofthesamemethod,butthissolutionhassomelimitations.Thefirstproblemisthatthenumberofpossiblepermutationsofagivenmethodisgrowingveryquickly(2n),makingthemverydifficulttomaintain.Thesecondproblemisthatoverloadsmustbedistinguishablefromeachother,socompilermayknowwhichoverloadtocall,sowhenamethoddefinesafewparameterswiththesametypewecan'tdefineallpossibleoverloads.That'swhyinJava,weoftenneedtopassmultiplenullvaluestoamethod:
//Java
printValue("abc",null,null,"!");
Multiplenullparametersprovideboilerplate.Suchasituationgreatlydecreasesmethodreadability.InKotlin,thereisnosuchproblem,becauseKotlinhasafeaturecalleddefaultargumentsandnamedargumentsyntax.
DefaultargumentsvaluesDefaultargumentsaremostlyknownfromC++,whichisoneoftheoldestlanguagessupportingit.Adefaultargumentprovidesavalueforaparameterincaseitisnotprovidedduringmethodcall.Eachfunctionparametercanhaveadefaultvalue.Itmightbeanyvaluethatismatchingaspecifiedtypeincludingnull.Thiswaywecansimplydefinefunctionsthatcanbecalledinmultipleways.Thisisanexampleofafunctionwithdefaultvalues:
funprintValue(value:String,inBracket:Boolean=true,
prefix:String="",suffix:String=""){
print(prefix)
if(inBracket){
print("(${value})")
}else{
print(value)
}
println(suffix)
}
Wecanusethisfunctionthesamewayasanormalfunction(afunctionwithoutdefaultargumentvalues)byprovidingvaluesforeachparameter(allarguments):
printValue("str",true,"","")//Prints:(str)
Thankstothedefaultargumentvalues,wecancallafunctionbyprovidingargumentsonlyforparameterswithoutdefaultvalues:
printValue("str")//Prints:(str)
Wecanalsoprovideallparameterswithoutdefaultvalues,andonlysomethathaveadefaultvalue:
printValue("str",false)//Prints:str
NamedargumentssyntaxSometimeswewantonlytopassavalueforthelastargument.Let'ssupposethatwedefinewanttovalueforasuffix,butnotforaprefixandinBracket(whichisdefinedbeforesuffix).Normallywewouldhavetoprovidevaluesforallpreviousparametersincludingthedefaultparametervalues:
printValue("str",true,true,"!")//Prints:(str)
Byusingnamedargumentsyntax,wecanpassspecificargumentsusingtheargumentname:
printValue("str",suffix="!")//Prints:(str)!
Thisallowsveryflexiblesyntax,wherewecansupplyonlychosenargumentswhencallingafunction(thatis,thefirstoneandthesecondfromtheend).Itisoftenusedtospecifywhatthisargumentisbecausesuchacallismorereadable:
printValue("str",inBracket=true)//Prints:(str)
printValue("str",prefix="Valueis")//Prints:Valueisstr
printValue("str",prefix="Valueis",suffix="!!")
//Prints:Valueisstr!!
Wecansetanyparameterswewantusingnamedparametersyntaxinanyorderaslongasallparameterswithoutdefaultvaluesareprovided.Theorderoftheargumentsisrelevant:
printValue("str",inBracket=true,prefix="Valueis")
//Prints:Valueis(str)
printValue("str",prefix="Valueis",inBracket=true)
//Prints:Valueis(str)
Orderofargumentsisdifferent,butbothprecedingcallsareequivalent.
Wecanalsousenamedargumentsyntaxtogetherwithclassiccall.Theonlyrestrictionisifwestartusingnamedsyntax,wecannotuseaclassiconefornextargumentsweareserving:
printValue("str",true,"")
printValue("str",true,prefix="")
printValue("str",inBracket=true,prefix="")
printValue("str",inBracket=true,"")//Error
printValue("str",inBracket=true,prefix="","")//Error
Thisfeatureallowsustocallmethodsinaveryflexiblewaywithouttheneedtodefinemultiplemethodoverloads.
ThenamedargumentsyntaximposessomeextraresponsibilityforKotlinprogrammers.Weneedtokeepinmindthatwhenwechangeaparametername,wemaycauseerrorsintheproject,becausetheparameternamemaybeusedinotherclasses.AndroidStudiowilltakecareofitifwerenametheparameterusingbuilt-inrefactoringtools,butthiswillworkonlyinsideourproject.TheKotlinlibrarycreatorsshouldbeverycarefulwhileusingnamedargumentsyntax.ChangeoftheparameternamewillbreaktheAPI.NotethatthenamedargumentsyntaxcannotbeusedwhencallingJavafunctions,becauseJavabytecodedoesnotalwayspreservenamesoffunctionparameters.
Top-levelfunctionsAnotherthingwecanobserveinasimpleHello,World!program,isthatthemainfunctionisnotlocatedinsideanyclass.InChapter2,LayingaFoundation,wealreadymentionedthatKotlincandefinevariousentitiesatthetoplevel.Afunctionthatisdefinedattop-leveliscalledthetop-levelfunction.Hereisanexampleofoneofthem:
//Test.kt
packagecom.example
funprintTwo(){
print(2)
}
Top-levelfunctionscanbeusedallaroundthecode(assumingthattheyarepublic,whatisdefaultvisibilitymodifier).Wecancalltheminthesamewayasfunctionsfromthelocalcontext.Toaccesstop-levelfunction,weneedtoexplicitlyimportitintoafilebyusingtheimportstatement.FunctionsareavailableincodehintlistinAndroidStudio,soimportsareautomaticallyaddedwhenafunctionisselected(used).Asanexample,let'sseeatop-levelfunctiondefinedinTest.ktanduseitinsidetheMain.ktfile:
//Test.kt
packagecom.example
funprintTwo(){
print(2)
}
//Main.kt
importcom.example.printTwo
funmain(args:Array<String>){
printTwo()
}
Top-levelfunctionsareoftenuseful,butitisimportanttousethemwisely.Keepinmindthatdefiningpublictop-levelfunctionswillincreasethenumberoffunctionsavailableincodehintlist(byhintlistImeanalistofmethodssuggestedbytheIDEashints,whenwearewritingcode).Itisbecausepublictop-levelfunctionsaresuggestedbytheIDEineverycontext(becausetheycanbeusedeverywhere).Ifthenameofthetop-levelfunctiondoesnotclearlystate
thatthisisatop-levelfunction,thenitmaybeconfusedwithamethodfromthelocalcontextandusedaccidentally.Herearesomegoodexamplesoftop-levelfunctions:
factorial
maxOfandminOflistOf
println
Herearesomeexamplesoffunctionsthatmaybepoorcandidatesfortoplevelfunctions:
sendUserData
showPossiblePlayers
ThisruleisapplicableonlyinKotlinobject-orientedprogrammingprojects.Infunction-orientedprogrammingprojects,thesearevalidtop-levelnames,butthenwesupposethatnearlyallfunctionsaredefinedinthetop-levelandnotasmethods.
Oftenwedefinefunctionswewanttouseonlyinspecificmodulesorspecificclasses.Tolimitfunctionvisibility(placewhereitcanbeused)wecanusevisibilitymodifiers.WewilldiscussvisibilitymodifiersinChapter4,ClassesandObjects.
Top-levelfunctionsunderthehoodWiththeAndroidprojects,KotliniscompiledtoJavabytecodethatrunsonDalvikVirtualMachine(beforeAndroid5.0)orAndroidRuntime(Android5.0andnewer).Bothvirtualmachinescanexecuteonlythecodethatisdefinedinsideaclass.TosolvethisproblemKotlincompilergeneratesclassesfortop-levelfunctions.TheclassnameisconstructedfromthefilenameandKtsuffix.Insidesuchaclass,allfunctionsandpropertiesarestatic.Forexample,let'ssupposethatwedefineafunctionwithinthePrinter.ktfile:
//Printer.kt
funprintTwo(){
print(2)
}
KotlincodeiscompiledintoJavabytecode.ThegeneratedbytecodewillbeanalogicaltothecodegeneratedfromthefollowingJavaclass:
//Java
publicfinalclassPrinterKt{//1
publicstaticvoidprintTwo(){//2
System.out.print(2);//3
}
}
1. PrinterKtisthenamemadefromthenameoffileandKtsuffix.2. Alltop-levelfunctionsandpropertiesarecompiledtostaticmethodsand
variables.3. printisaKotlinfunction,butsinceitisaninlinefunction,itscallis
replacedbyitsbodyduringcompilationtime.AnditsbodyincludesonlySystem.out.printlncall.
InlinefunctionswillbedescribedinChapter5,FunctionsasaFirstClassCitizen.
KotlinclassatJavabytecodelevelwillcontainmoredata(forexample,nameofparameters).WecanalsoaccessKotlintop-levelfunctionsfromJavafilesbyprefixingafunctioncallwiththeclassname:
//Javafile,callinsidesomemethod
PrinterKt.printTwo()
Thisway,Kotlintop-levelfunctionscallsfromJavaarefullysupported.Aswecansee,KotlinisreallyinteroperablewithJava.TomakeKotlintop-levelfunctionsusagemorecomfortableinJava,wecanaddanannotationthatwillchangethenameofaJVMgeneratedclass.Thiscomesinhandywhenmakingusageoftop-levelKotlinpropertiesandfunctionsfromJavaclasses.Thisannotationlooksasfollows:
@file:JvmName("Printer")
WeneedtoaddtheJvmNameannotationatthetopofthefile(beforepackagename).Whenthisisapplied,thenameofthegeneratedclasswillbechangedtoPrinter.ThisallowsustocalltheprintTwofunctioninJavausingPrinterastheclassname:
//Java
Printer.printTwo()
Sometimeswearedefiningtop-levelfunctions,andwewanttodefinetheminseparatefiles,butwealsowanttheminthesameclassaftercompilationtoJVM.Thisispossibleifweusethefollowingannotationintopofthefile:
@file:JvmMultifileClass
Forexample,let'sassumethatwearemakingalibrarywithmathematicalhelpersthatwewanttousefromJava.Wecandefinethefollowingfiles:
//Max.kt
@file:JvmName("Math")
@file:JvmMultifileClass
packagecom.example.math
funmax(n1:Int,n2:Int):Int=if(n1>n2)n1elsen2
//Min.kt
@file:JvmName("Math")
@file:JvmMultifileClass
packagecom.example.math
funmin(n1:Int,n2:Int):Int=if(n1<n2)n1elsen2
AndwecanuseitfromJavaclassesthisway:
Math.min(1,2)
Math.max(1,2)
Thankstothis,wecankeepfilesshortandsimple,whilekeepingthemalleasytousefromJava.
TheJvmNameannotationtochangegeneratedclassnamesisespeciallyusefulwhenwecreatelibrariesinKotlinthatarealsodirectedtobeusedinJavaclasses.Itcanbeusefulincaseofnameconflictstoo.SuchasituationcanoccurwhenwecreatebothanX.ktfilewithsometop-levelfunctionsorpropertiesandanXKtclassinthesamepackage.ButitisrareandshouldnevertakeplacesincethereisaconventionthatnoclassesshouldhaveKtsuffix.
LocalfunctionsKotlinallowsdefiningfunctionsinmanycontexts.Wecandefinefunctionsattop-level,asmembers(insidetheclass,interface,andsoon),andinsideotherfunction(localfunction).Considerthefollowingexampleofthedefinitionoflocalfunction:
funprintTwoThreeTimes(){
funprintThree(){//1
print(3)
}
printThree()//2
printThree()//2
}
1. printThreeisalocalfunction,becauseitislocatedinsideanotherfunction.2. Localfunctionsarenotaccessiblefromoutsidethefunctiontheywere
declaredin.
Elementsaccessibleinsidelocalfunctionsdon'thavetobepassedfromenclosingfunctionsasargumentsbecausetheyareaccessibledirectly.Forexample:
funloadUsers(ids:List<Int>){
vardownloaded:List<User>=emptyList()
funprintLog(comment:String){
Log.i("loadUsers(withids$ids):$comment\nDownloaded:
$downloaded")//1
}
for(idinids){
printLog("Startdownloadingforid$id")
downloaded+=loadUser(id)
printLog("Finisheddownloadingforid$id")
}
}
1. Localfunctioncanaccesscommentparameterandlocalvariables(downloadedandIDs),definedinsideanenclosingfunction.
IfwewouldliketodefineprintLogasatop-levelfunctionthenwewouldhavetopassasargumentsbothidsanddownloaded:
funloadUsers(ids:List<Int>){
vardownloaded:List<User>=emptyList()
for(idinids){
printLog("Startdownloadingforid$id",downloaded,ids)
downloaded+=loadUser(id)
printLog("Finisheddownloadingfor
id$id",downloaded,ids))
}
}
funprintLog(state:String,downloaded:List<User>,ids:List<Int>)
{
Log.i("loadUsers(withids$ids):
$state\nDownloaded:downloaded")
}
Thisimplementationisnotonlylonger,butalsohardertomaintain.ChangesinprintLogmightdemanddifferentparameters,andachangeinparametersdemandschangesinargumentsinthisfunctioncall.Also,ifwechangetheloadUsersparametertypethatisusedinprintLogthenwewillneedtoalsochangetheparameterofprintLog.TherewouldbenosuchproblemsifprintLogwasalocalfunction.Thisexplainswhenlocalfunctionsshouldbeused:Whenweareextractingfunctionalitythatisusedonlybyasinglefunction,andthatfunctionalityisusingelements(variables,values,parameters)ofthisfunction.Also,localfunctionsareallowedtomodifylocalvariables.Likeinthisexample:
funmakeStudentList():List<Student>{
varstudents:List<Student>=emptyList()
funaddStudent(name:String,state:Student.State=
Student.State.New){
students+=Student(name,state,courses=emptyList())
}
//...
addStudent("AdamSmith")
addStudent("DonaldDuck")
//...
returnstudents
}
Thisway,wecanextractandreusefunctionalitythatcouldnotbeextractedinJava.Itisgoodtorememberaboutlocalfunctions,becausetheysometimesallowcodeextractionthatishardtoimplementinotherways.
NothingreturntypeSometimesweneedtodefineafunctionthatisalwaysthrowingexceptions(neverterminatingnormally).Tworeal-lifeusecasesare:
Functionsthatsimplifyerrorthrowing.Thisisespeciallyusefulinlibrarieswhereerrorsystemisimportantandthereisaneedtoprovidemoredataabouterroroccurrence.(AsanexamplelookatthethrowErrorfunctionpresentedinthissection).Functionsusedforthrowingerrorsinunittests.Thisisusefulwhenweneedtotesterrorhandlinginourcode.
Forthesekindsofsituations,thereisaspecialclasscalledNothing.TheNothingclassisemptytype(uninhabitedtype),meaningithasnoinstances.AfunctionthathasNothingreturntypewon'treturnanythinganditwillneverreachreturnstatement.Itcanonlythrowanexception.ThisiswhywhenweseethatfunctionisreturningNothing,thenitisdesignedtothrowexceptions.Thiswaywecandistinguishfunctionsthatdonotreturnavalue(likeJava'svoid,Kotlin'sUnit)fromfunctionsthatneverterminate(returnsNothing).Letushavealookatanexampleoffunctionsthatmightbeusedtosimplifyerrorthrowinginunittests:
funfail():Nothing=throwError()
Andfunctionsthatareconstructingcomplexerrormessagesusingelementsavailableincontextwhereitisdefined(inclassorfunction):
funprocessElement(element:Element){
funthrowError(message:String):Nothing
=throwProcessingError("Errorinelement$element:$message")
//...
if(element.kind!=ElementKind.METHOD)
throwError("Notamethod")
//...
}
Thiskindoffunctionisthatitcanbeused,justlikeathrowstatement,asanalternativethatisnotinfluencingfunctionreturnedtype:
fungetFirstCharOrFail(str:String):Char
=if(str.isNotEmpty())str[0]elsefail()
valname:String=getName()?:fail()
valenclosingElement=element.enclosingElement?:throwError("Lackofenclosingelement")
Howisitpossible?ThisisaspecialtraitoftheNothingclass,whichisactingasifitisasubtypeofallthepossibletypes:bothnullableandnot-nullable.ThisiswhyNothingisreferredasanemptytype,whichmeansthatnovaluecanhavethistypeatruntime,andit'salsoasubtypeofeveryotherclass.
TheconceptofuninhabitedtypeisnewintheworldofJava,andthisiswhyitmightbeconfusing.Theideaisactuallyprettysimple.TheNothinginstanceisneverexisting,whilethereisonlyanerrorthatmightbereturnedfromfunctionsthatarespecifyingitasareturntype.AndthereisnoneedforNothingaddedtosomethingtoinfluenceitstype.
SummaryInthischapter,we'veseenhowtodefineandusefunctions.Welearnedhowfunctionscanbedefinedonthetop-levelorinsideotherfunctions.Therewasalsoadiscussionondifferentfeaturesconnectedtofunctions--varargparameters,defaultnames,andnamedargumentsyntax.Finally,wesawsomeKotlinspecialreturntypes:Unit,whichisequivalentofJavavoid,andNothing,whichisatypethatcannotbedefinedandmeansthatnothingcanbereturned(onlyexceptions).
Inthenextchapter,wearegoingtoseehowclassesaredefinedinKotlin.ClassesarealsospeciallysupportedbytheKotlinlanguage,andtherearelotsofimprovementsintroducedoverJavadefinitions.
ClassesandObjectsTheKotlinlanguageprovidesfullsupportforOOPprogramming.Wewillreviewpowerfulstructuresthatallowustosimplifydatamodeldefinitionandoperateonitinaneasyandflexibleway.We'lllearnhowKotlinsimplifiesandimprovesimplementationsofmanyconceptsknownfromJava.Wewilltakealookatdifferenttypeofclasses,properties,initializerblocks,andconstructors.Wewilllearnaboutoperatoroverloadingandinterfacedefaultimplementations.
Inthischapter,wewillcoverthefollowingtopics:
ClassdeclarationPropertiesPropertyaccesssyntaxConstructorsandinitializersblocksConstructorsInheritanceInterfacesDataclassesDestructivedeclarationsOperatoroverloadingObjectdeclarationObjectexpressionCompanionobjectsEnumclassesSealedclassesNestedclasses
ClassesClassesareafundamentalbuildingblockofOOP.Infact,KotlinclassesareverysimilartoJavaclasses.Kotlin,however,allowsmorefunctionalitytogetherwithsimplerandmuchmoreconcisesyntax.
ClassdeclarationClassesinKotlinaredefinedusingtheclasskeyword.Thefollowingisthesimplestclassdeclaration--anemptyclassnamedPerson:
classPerson
DefinitionofPersondoesnotcontainanybody.Still,itcanbeinstantiatedusingadefaultconstructor:
valperson=Person()
EvensuchasimpletaskasclassinstantiationissimplifiedinKotlin.UnlikeJava,Kotlindoesnotrequirethenewkeywordtocreateaclassinstance.DuetostrongKotlininteroperabilitywithJava,wecaninstantiateclassesdefinedinJavaandKotlinexactlythesameway(withoutthenewkeyword).Thesyntaxusedtoinstantiateaclassdependsontheactuallanguageusedtocreateclassinstance(KotlinorJava),notthelanguagetheclasswasdeclaredin:
//InstantiateKotlinclassinsideJavafile
Personperson=newPerson()
//InstantiateclassinsideKotlinfile
varperson=Person()
ItistheruleofthumbtousethenewkeywordinsideaJavafileandneverusethenewkeywordinsideaKotlinfile.
PropertiesPropertyisjustacombinationofabackingfieldanditsaccessors.Itcouldbeabackingfieldwithbothagetterandasetterorabackingfieldwithonlyoneofthem.Propertiescanbedefinedatthetop-level(directlyinsidefile)orasamember(forexample,insideclass,interface,andsoon).
Ingeneral,itisadvisabletodefineproperties(privatefieldswithgetters/setters)insteadofaccessingpublicfieldsdirectly(AccordingtoEffectiveJava,byJoshuaBloch,book'sitem14:inpublicclasses,useaccessormethods,notpublicfields).
Javagetterandsetterconventionsforprivatefields
Getter:Aparameterlessmethodwithanamethatcorrespondstopropertynameandagetprefix(foraBooleanpropertytheremightbeanisprefixusedinstead)
Setter:Single-argumentmethodswithnamesstartingwithset:forexample,setResult(StringresultCode)
Kotlinguardsthisprinciplebylanguagedesign,becausethisapproachprovidesvariousencapsulationbenefits:
AbilitytochangeinternalimplementationwithoutchanginganexternalAPIEnforcesinvariants(callmethodsthatvalidateobjectsstate)Abilitytoperformadditionalactionswhenaccessingmember(forexample,logoperation)
Todefineatop-levelproperty,wesimplydefineitintheKotlinfile:
//Test.kt
valname:String
Let'simaginethatweneedaclasstostorebasicdataregardingaperson.ThisdatamaybedownloadedfromanexternalAPI(backend)orretrievedfroma
localdatabase.Ourclasswillhavetodefinetwo(member)properties,nameandage.Let'slookattheJavaimplementationfirst:
publicclassPerson{
privateintage;
privateStringname;
publicPerson(Stringname,intage){
this.name=name;
this.age=age;
}
publicintgetAge(){
returnage;
}
publicvoidsetAge(intage){
this.age=age;
}
publicStringgetName(){
returnname;
}
publicvoidsetName(Stringname){
this.name=name;
}
}
Thisclasscontainsonlytwoproperties.SincewecanmaketheJavaIDEgenerateaccessorscodeforus,atleastwedon'thavetowritethecodebyourselves.However,theproblemwiththisapproachisthatwecannotgetalongwithouttheseautomaticallygeneratedchunks,andthatmakesthecodeveryverbose.We(developers)spendmostofourtimejustreadingthecode,notwritingit,soreadingredundantcodewastesalotofvaluabletime.Alsoasimpletasksuchasrefactoringthepropertynamebecomesalittlebittrickier,becausetheIDEmightnotupdateconstructorparameternames.
Fortunately,boilerplatecodecanbedecreasedsignificantlybyusingKotlin.Kotlinsolvesthisproblembyintroducingtheconceptofpropertiesthatisbuiltintothelanguage.Let'slookataKotlinequivalentoftheprecedingJavaclass:
classPerson{
varname:String
varage:Int
constructor(name:String,age:Int){
this.name=name
this.age=age
}
}
ThisisanexactequivalentoftheprecedingJavaclass:
TheconstructormethodisequivalentoftheJavaconstructorthatiscalledwhenanobjectinstanceiscreatedGettersandsettersaregeneratedbytheKotlincompiler
Wecanstilldefinecustomimplementationsofgettersandsetters.WewilldiscussthisinmoredetailintheCustomgetters/setterssection.
Alltheconstructorsthatwehavealreadydefinedarecalledsecondaryconstructors.Kotlinalsoprovidesalternative,veryconcisesyntaxfordefiningconstructors.Wecandefineaconstructor(withallparameters)aspartoftheclassheader.Thiskindofconstructoriscalledaprimaryconstructor.Let'smoveapropertydeclarationfromthesecondaryconstructorintotheprimaryconstructortomakeourcodealittlebitshorter:
classPersonconstructor(name:String,age:Int){
varname:String
varage:Int
init{
this.name=name
this.age=age
println("Personinstancecreated")
}
}
InKotlin,theprimaryconstructor,asopposedtothesecondaryconstructor,can'tcontainanycode,soallinitializationcodemustbeplacedinsidetheinitializerblock(init).Aninitializerblockwillbeexecutedduringclasscreation,sowecanassignconstructorparameterstofieldsinsideit.
Tosimplifycode,wecanremovetheinitializerblockandaccessconstructorparametersdirectlyinpropertyinitializers.Thisallowsustoassignconstructorparameterstoafield:
classPersonconstructor(name:String,age:Int){
varname:String=name
varage:Int=age
}
Wemanagedtomakethecodeshorter,butitstillcontainsalotofboilerplate,becausetypedeclarationsandpropertynamesareduplicated(constructorparameter,fieldassignment,andfielditself).Whenpropertiesdoesnothaveany
customgettersorsetterswecandefinethemdirectlyinsideprimaryconstructorbyaddingvalorvarmodifier:
classPersonconstructor(varname:String,varage:Int)
Finally,iftheprimaryconstructordoesnothaveanyannotations(@Inject,andsoon)orvisibilitymodifiers(public,private,andsoon),thentheconstructorkeywordcanbeomitted:
classPerson(varname:String,varage:Int)
Whentheconstructortakesafewparameters,itisgoodpracticetodefineeachparameterinanewlinetoimprovecodereadabilityanddecreasechanceofpotentialmergeconflicts(whenmergingbranchesfromsourcecoderepository):
classPerson(
varname:String,
varage:Int
)
Summingup,theprecedingexampleisequivalentoftheJavaclasspresentedatthebeginningofthissection--bothpropertiesaredefineddirectlyintheclassprimaryconstructorandKotlincompilerdoesalltheworkforus--itgeneratesappropriatefieldsandaccessors(getters/setters).
Notethatthisnotationcontainsonlythemostimportantinformationaboutthisdatamodelclass--itsname,parameternames,types,andmutability(val/var)information.Implementationhasnearlyzeroboilerplate.Thismakestheclassveryeasytoread,understand,andmaintain.
Read-writeversusread-onlypropertyAllthepropertiesinthepreviousexamplesweredefinedasread-write(asetterandagetteraregenerated).Todefineread-onlypropertiesweneedtousethevalkeyword,soonlygetterwillbegenerated.Let'slookatasimpleexample:
classPerson(
varname:String,
//Read-writeproperty(generatedgetterandsetter)
valage:Int//Read-onlyproperty(generatedgetter)
)
\\usage
valperson=Person("Eva",25)
valname=person.name
person.name="Kate"
valage=person.age
person.age=28\\error:read-onlyproperty
Kotlindoesnotsupportwrite-onlyproperties(propertiesofwhichonlysetterisgenerated).
Keyword Read Write
var Yes Yes
val Yes No
(unsupported) No Yes
PropertyaccesssyntaxbetweenKotlinandJavaAnotherbigimprovementintroducedbyKotlinisthewayofaccessingproperties.InJava,wewouldaccesspropertyusingthecorrespondingmethod(setSpeed/getSpeed).Kotlinpromotespropertyaccesssyntax,whichisamoreexpressivewayofaccessingproperties.Let'scomparebothapproaches,assumingwehaveasimpleCarclassthathasasinglespeedproperty:
classCar(varspeed:Double)
//Javaaccesspropertiesusingmethodaccesssyntax
Carcar=newCar(7.4)
car.setSpeed(9.2)
Doublespeed=car.getSpeed();
//Kotlinaccesspropertiesusingpropertyaccesssyntax
valcar:Car=Car(7.4)
car.speed=9.2
valspeed=car.speed
Aswecansee,inKotlinthereisnoneedtoaddget,setprefixesandparenthesestoaccessormodifyanobjectproperty.Usingpropertyaccesssyntaxallowsfordirectusageofincrement(++)anddecrement(--)operatorstogetherwithpropertyaccess:
valcar=Car(7.0)
println(car.speed)//prints7.0
car.speed++
println(car.speed)//prints8.0
car.speed--
car.speed--
println(car.speed)//prints:6.0
IncrementanddecrementoperatorsTherearetwokindsofincrement(++)anddecrement(--)operators:pre-increment/pre-decrementwheretheoperatorisdefinedbeforetheexpression,andpost-increment/post-decrementwheretheoperatorisdefinedaftertheexpression:
++speed//preincrement
--speed//predecrement
speed++//postincrement
speed--//postdecrement
Intheprecedingexample,usingpost-versuspre-increment/decrementwouldchangenothingbecausethoseoperationsareexecutedinsequence.Butthismakesahugedifferencewhentheincrement/decrementoperatoriscombinedwithafunctioncall.
Inthepre-incrementoperator,speedisretrieved,incremented,andpassedtoafunctionasanargument:
varspeed=1.0
println(++speed)//Prints:2.0
println(speed)//Prints:2.0
Inpost-incrementoperatorspeedisretrieved,passedtoafunctionasanargument,andthenitisincremented,sotheoldvalueispassedtoafunction:
varspeed=1.0
println(speed++)//Prints:1.0
println(speed)//Prints:2.0
Thisworksinananalogicalwayforpre-decrementandpost-decrementoperators.
PropertyaccesssyntaxisnotlimitedonlytoclassesdefinedinKotlin.EachmethodthatfollowstheJavaconventionsforgettersandsettersisrepresentedasapropertyinKotlin.
ThismeansthatwecandefineaclassinJavaandaccessitspropertiesinKotlin
usingpropertyaccesssyntax.Let'sdefineaJavaFishclasswithtwoproperties,sizeandisHungry,andlet'sinstantiatethisclassinKotlinandaccesstheproperties:
//Javaclassdeclaration
publicclassFish{
privateintsize;
privatebooleanhungry;
publicFish(intsize,booleanisHungry){
this.size=size;
this.hungry=isHungry;
}
publicintgetSize(){
returnsize;
}
publicvoidsetSize(intsize){
this.size=size;
}
publicbooleanisHungry(){
returnhungry;
}
publicvoidsetHungry(booleanhungry){
this.hungry=hungry;
}
}
//Kotlinclassusage
valfish=Fish(12,true)
fish.size=7
println(fish.size)//Prints:7
fish.isHungry=true
println(fish.isHungry)//Prints:true
Thisworksbothways,sowecandefinetheFishclassinKotlinusingveryconcisesyntaxandaccessitinausualJavaway,becausetheKotlincompilerwillgeneratealltherequiredgettersandsetters:
//Kotlinclassdeclaration
classFish(varsize:Int,varhungry:Boolean)
//classusageinJava
Fishfish=newFish(12,true);
fish.setSize(7);
System.out.println(fish.getSize());
fish.setHungry(false);
System.out.println(fish.getHungry());
Aswecansee,syntaxusedtoaccesstheclasspropertydependsontheactuallanguagethattheclassuses,notthelanguagethattheclasswasdeclaredin.ThisallowsformoreidiomaticusageofmanyclassesdefinedintheAndroid
framework.Let'sseesomeexamples:
Javamethodaccesssyntax Kotlinpropertyaccesssyntax
activity.getFragmentManager() activity.fragmentManager
view.setVisibility(Visibility.GONE) view.visibility=Visibility.GONE
context.getResources().getDisplayMetrics().density context.resources.displayMetrics.density
PropertyaccesssyntaxresultsinmoreconcisecodethatdecreasestheoriginalJavalanguagecomplexity.NoticethatitisstillpossibletousemethodaccesssyntaxwithKotlinalthoughpropertyaccesssyntaxisoftenthebetteralternative.
TherearesomemethodsintheAndroidframeworkthatusetheisprefixfortheirname;inthiscasesBooleanpropertiesalsohavetheisprefix:
classMainActivity:AppCompatActivity(){
overridefunonDestroy(){//1
super.onDestroy()
isFinishing()//methodaccesssyntax
isFinishing//propertyaccesssyntax
finishing//error
}
}
1. Kotlinmarksoverriddenmembersusingtheoverridemodifier,not@OverrideannotationlikeJava.
Althoughusingfinishingwouldbethemostnaturalandconsistentapproach,it'simpossibletouseitbydefaultduetopotentialconflicts.
Anothercasewherewecan'tusethepropertyaccesssyntaxiswhenthepropertydefinesonlysetterwithoutgetter,becauseKotlindoesnotsupportwrite-onlyproperties,asinthisexample:
fragment.setHasOptionsMenu(true)
fragment.hasOptionsMenu=true//Error!
Customgetters/settersSometimeswewanttohavemorecontrolaboutpropertyusage.Wemaywanttoperformotherauxiliaryoperationswhenusingproperty;forexample,verifyavaluebeforeit'sassignedtoafield,logthewholeoperation,orinvalidateaninstancestate.Wecandoitbyspecifyingcustomsettersand/orgetters.Let'saddtheecoRatingpropertytoourFruitclass.Inmostcases,wewouldaddthispropertytotheclassdeclarationheaderlikethis:
classFruit(varweight:Double,
valfresh:Boolean,
valecoRating:Int)
Ifwewanttodefinecustomgettersandsetters,weneedtodefineapropertyintheclassbodyinsteadoftheclassdeclarationheader.Let'smovetheecoRatingpropertyintotheclassbody:
classFruit(varweight:Double,valfresh:Boolean,ecoRating:Int)
{
varecoRating:Int=ecoRating
}
Whenthepropertyisdefinedinsidethebodyofaclass,wehavetoinitializeitwithvalue(evennullablepropertiesneedtobeinitializedwithanullvalue).Wecanprovidethedefaultvalueinsteadoffillingapropertywiththeconstructorargument:
classFruit(varweight:Double,valfresh:Boolean){
varecoRating:Int=3
}
Wecanalsocomputedefaultvaluesbasedonsomeotherproperties:
classApple(varweight:Double,valfresh:Boolean){
varecoRating:Int=when(weight){
in0.5..2.0->5
in0.4..0.5->4
in0.3..0.4->3
in0.2..0.3->2
else->1
}
}
Differentvalueswillbesetfordifferentweightconstructorarguments.
Whenapropertyisdefinedinaclassbody,thetypedeclarationcanbeomitted,becauseitcanbeinferredfromthecontext:
classFruit(varweight:Double){
varecoRating=3
}
Let'sdefineacustomgetterandsetterwiththedefaultbehaviorthatwillbetheequivalentoftheprecedingproperty:
classFruit(varweight:Double){
varecoRating:Int=3
get(){
println("gettervalueretrieved")
returnfield
}
set(value){
field=if(value<0)0elsevalue
println("setternewvalueassigned$field")
}
}
//Usage
valfruit=Fruit(12.0)
valecoRating=fruit.ecoRating
//Prints:gettervalueretrieved
fruit.ecoRating=3;
//Prints:setternewvalueassigned3
fruit.ecoRating=-5;
//Prints:setternewvalueassigned0
Insidethegetandsetblock,wecanhaveaccesstoaspecialvariablecalledfield,whichreferstothecorrespondingbackingfieldoftheproperty.NoticethattheKotlinpropertydeclarationiscloselypositionedtoacustomgetter/setter.ThiscontradictswithJavaandsolvestheissuewherethefielddeclarationisusuallyatthetopofthefilecontainingclassandcorrespondinggetter/setterisatthebottomofthisfile,sowecan'treallyseethemonasinglescreenandthuscodeismoredifficulttoread.Apartfromthatlocation,KotlinpropertybehaviorisquitesimilartoJava.Eachtimeweretrieve,valuefromtheecoRatingproperty,agetblockwillbeexecuted,andeachtimeweassignanewvaluetotheecoRatingproperty,asetblockwillbeexecuted.
Thisisaread-writeproperty(var),soitmaycontainbothcorrespondinggettersandsetters.Incaseweexplicitlydefineonlyoneofthem,thedefaultimplementationwillbeusedforanother.
Tomakeavaluecomputedeachtimewhenapropertyvalueisretrieved,weneedtoexplicitlydefinegetter:
classFruit(varweight:Double){
valheavy//1
get()=weight>20
}
//usage
varfruit=Fruit(7.0)
println(fruit.heavy)//prints:false
fruit.weight=30.5
println(fruit.heavy)//prints:true
1. SinceKotlin1.1typecanbeomitted(itistobeinferred).
ThegetterversuspropertydefaultvalueIntheprecedingexample,weusedgetter,sothepropertyvalueiscalculatedeachtimethevalueisretrieved.Byomittinggetterwecancreateadefaultvaluefortheproperty.Thisvaluewillbecomputedonlyonceduringclasscreationanditwillneverchange(changingtheweightpropertywillhavenoeffectontheisHeavypropertyvalue):
classFruit(varweight:Double){
valisHeavy=weight>20
}
varfruit=Fruit(7.0)
println(fruit.isHeavy)//Prints:false
fruit.weight=30.5
println(fruit.isHeavy)//Prints:false
Thistypeofpropertydoeshaveabackingfield,becauseitsvalueisalwayscomputedduringobjectcreation.Wecanalsocreateread-writepropertieswithoutabackingfield:
classCar{
varusable:Boolean=true
varinGoodState:Boolean=true
varcrashed:Boolean
get()=!usable&&!inGoodState
set(value){
usable=false
inGoodState=false
}
}
Thistypeofpropertydoesnothaveabackingfield,becauseitsvalueisalwayscomputedusinganotherproperty.
Late-initializedpropertiesSometimesweknowthatapropertywon'tbenull,butitwon'tbeinitializedwiththevalueatdeclarationtime.Let'slookatcommonAndroidexamples--retrievingreferencetoalayoutelement:
classMainActivity:AppCompatActivity(){
privatevarbutton:Button?=null
overridefunonCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
button=findViewById(R.id.button)asButton
}
}
Thebuttonvariablecan'tbeinitializedatdeclarationtime,becausetheMainActivitylayoutisnotyetinitialized.WecanretrievereferencetothebuttondefinedinthelayoutinsidetheonCreatemethod,buttodoitweneedtodeclareavariableasnullable(Button?).
Suchanapproachseemsquiteimpractical,becauseaftertheonCreatemethodiscalledabuttoninstanceisavailableallthetime.However,theclientstillneedstousethesafecalloperatororothernullitycheckstoaccessit.
Toavoidnullitycheckswhenaccessingaproperty,weneedawaytoinformtheKotlincompilerthatthisvariablewillbefilledbeforeusage,butitsinitializationwillbedelayedintime.Todothis,wecanusethelateinitmodifier:
classMainActivity:AppCompatActivity(){
privatelateinitvarbutton:Button
overridefunonCreate(savedInstanceState:Bundle?){
button=findViewById(R.id.button)asButton
button.text="ClickMe"
}
}
Now,withthepropertymarkedaslateinit,wecanaccessourapplicationinstancewithoutperformingnullitychecks.
Thelateinitmodifiertellsthecompilerthatthispropertyisnon-nullable,butits
initializationisdelayedintime.Naturally,whenwetrytoaccessthepropertybeforeitisinitialized,theapplicationwillthrowUninitializedPropertyAccessException.Thisisfine,becauseweassumethatthisscenarioshouldnothappen.
Ascenariowhereavariableinitializationisnotpossibleatdeclarationtimeisquitecommonanditisnotalwaysrelatedtoviews.PropertiescanbeinitializedthroughDependencyInjection,orviathesetupmethodofaunittest.Insuchscenarios,wecannotsupplyanon-nullablevalueintheconstructor,butwestillwanttoavoidnullitychecks.
Thelateinitpropertyandframeworks
ThelateinitpropertyisalsohelpfulwhenapropertyisinjectedbytheDependencyInjectionframework.ThepopularAndroidDependencyInjectionframework,Dagger,usesthe@Injectannotationtomarkpropertiesthatneedtobeinjected:
@InjectlateinitvarlocationManager:LocationManager
Weknowthatthepropertywillneverbenull(becauseitwillbeinjected),buttheKotlincompilerdoesnotunderstandthisannotation.
Similarscenarioshappenwiththepopularframework,Mockito:
@MocklateinitvarmockEventBus:EventBus
Thevariablewillbemocked,butitwillhappensometimelater,aftertestclasscreation.
AnnotatingpropertiesKotlingeneratesmultipleJVMbytecodeelementsfromasingleproperty(privatefield,getter,setter).Sometimestheframeworkannotationprocessororthereflection-basedlibraryrequiresaparticularelementtobedefinedasapublicfield.AgoodexampleofsuchbehavioristheJUnittestframework.Itrequiresrulestobeprovidedthroughatestclassfieldoragettermethod.WemayencounterthisproblemwhendefiningActivityTestRuleorMockito's(mockingframeworkforunittests)Ruleannotation:
@Rule
valactivityRule=ActivityTestRule(MainActivity::class.Java)
TheprecedingcodeannotatestheKotlinpropertythatJUnitwon'trecognize,soActivityTestRulecan'tbeproperlyinitialized.TheJUnitannotationprocessorexpectstheRuleannotationonthefieldorgetter.Thereareafewwaystosolvethisproblem.WecanexposetheKotlinpropertyasaJavafieldbyannotatingitwiththe@JvmFieldannotation:
@JvmField@Rule
valactivityRule=ActivityTestRule(MainActivity::class.Java)
Thefieldwillhavethesamevisibilityastheunderlyingproperty.Thereareafewlimitationsregarding@JvmFieldannotationusage.Wecanannotateapropertywith@JvmFieldifithasabackingfield,itisnotprivate,doesnothaveopen,override,orconstmodifiers,andisnotadelegatedproperty.
Wecanalsoannotategetterbyaddinganannotationdirectlytogetter:
valactivityRule
@Ruleget()=ActivityTestRule(MainActivity::class.java)
Ifwedon'twanttodefinegetter,wecanstilladdanannotationtogetterusingtheuse-sitetarget(get).Bydoingso,wesimplyspecifywhichelementgeneratedbytheKotlincompilerwillbeannotated:
@get:Rule
valactivityRule=ActivityTestRule(MainActivity::class.Java)
InlinepropertiesWecanoptimizepropertycallsbyusingtheinlinemodifier.Duringcompilationeachpropertycallwillbeoptimized.Insteadofreallycallingaproperty,thecallwillbereplacedwiththepropertybody:
inlinevalnow:Long
get(){
println("Timeretrieved")
returnSystem.currentTimeMillis()
}
Withinlineproperty,weareusingtheinlinemodifier.Theprecedingcodewillbecompiledto:
println("Timeretrieved")
System.currentTimeMillis()
Inliningimprovesperformance,becausethereisnoneedtocreateadditionalobjects.Nogetterwillbeinvoked,becausethebodywouldreplacethepropertyusage.Inlininghasonelimitation--itcanbeonlyappliedtopropertiesthatdonothaveabackingfield.
ConstructorsKotlinallowsustodefineclasseswithoutanyconstructors.Wecanalsodefineaprimaryconstructorandoneormoresecondaryconstructors:
classFruit(valweight:Int){
constructor(weight:Int,fresh:Boolean):this(weight){}
}
//classinstantiation
valfruit1=Fruit(10)
valfruit2=Fruit(10,true)
Declaringpropertiesisnotallowedforsecondaryconstructors.Ifweneedapropertythatisinitializedbysecondaryconstructors,wemustdeclareitintheclassbody,andwecaninitializeitinthesecondaryconstructorbody.Let'sdefinethefreshproperty:
classTest(valweight:Int){
varfresh:Boolean?=null
//definefreshpropertyinclassbody
constructor(weight:Int,fresh:Boolean):this(weight){
this.fresh=fresh
//assignconstructorparametertofreshproperty
}
}
Noticethatwedefinedourfreshpropertyasnullable,becausewhenaninstanceoftheobjectwillbecreatedusingaprimaryconstructorthefreshpropertywillbenull:
valfruit=Fruit(10)
println(fruit.weight)//prints:10
println(fruit.fresh)//prints:null
Wecanalsoassignthedefaultvaluetothefreshpropertytomakeitnon-nullable:
classFruit(valweight:Int){
varfresh:Boolean=true
constructor(weight:Int,fresh:Boolean):this(weight){
this.fresh=fresh
}
}
valfruit=Fruit(10)
println(fruit.weight)//prints:10
println(fruit.fresh)//prints:true
Whenaprimaryconstructorisdefined,everysecondaryconstructormustcalltheprimaryconstructorimplicitlyorexplicitly.Animplicitcallmeansthatwecalltheprimaryconstructordirectly.Anexplicitcallmeansthatwecallanothersecondaryconstructorthatcallsprimaryconstructor.Tocallanotherconstructor,weusethethiskeyword:
classFruit(valweight:Int){
constructor(weight:Int,fresh:Boolean):this(weight)//1
constructor(weight:Int,fresh:Boolean,color:String):
this(weight,fresh)//2
}
1. Calltoprimaryconstructor2. Calltosecondaryconstructor
Iftheclasshasnoprimaryconstructorandthesuperclasshasanon-emptyconstructor,theneachsecondaryconstructorhastoinitializethebaseclassusingthesuperkeywordorcallanotherconstructorthatdoesthat:
classProductView:View{
constructor(ctx:Context):super(ctx)
constructor(ctx:Context,attrs:AttributeSet):
super(ctx,attrs)
}
Aviewexamplecanbegreatlysimplifiedbyusingthe@JvmOverloadsannotationthatwillbedescribedinthe@JvmOverloadssection.
Bydefault,thisgeneratedconstructorwillbepublic.Ifwewanttopreventthegenerationofsuchanimplicitpublicconstructor,wehavetodeclareanemptyprimaryconstructorwithaprivateorprotectedvisibilitymodifier:
classFruitprivateconstructor()
Tochangetheconstructorvisibility,weneedtoexplicitlyusetheconstructorkeywordintheclassdefinitionheader.Theconstructorkeywordisalsorequiredwhenwewanttoannotateaconstructor.AcommonexampleistoannotateaclassconstructorusingtheDagger(DependencyInjectionframework)@Injectannotation:
classFruit@Injectconstructor()
Boththevisibilitymodifierandannotationcanbeappliedatthesametime:
classFruit@Injectprivateconstructor{
varweight:Int?=null
}
PropertyversusconstructorparameterTheimportantthingtonoticeisthefactthatifweremovethevar/valkeywordfromconstructorpropertydeclaration,we'llendupwithaconstructorparameterdeclaration.Thismeansthatthepropertywillbechangedintoconstructorparameter,sonoaccessorswillbegeneratedandwewillnotbeabletoaccessthepropertyontheclassinstance:
classFruit(varweight:Double,fresh:Boolean)
valfruit=Fruit(12.0,true)
println(fruit.weight)
println(fruit.fresh)//error
Intheprecedingexample,wehaveanerrorbecausefreshismissingavalorvarkeyword,soitisaconstructorparameter,notaclasspropertysuchasweight.Thefollowingtablesummarizesthecompileraccessorgeneration:
Classdeclaration Gettergenerated
Settergenerated Type
classFruit(name:String) No No Constructorparameter
classFruit(val
name:String) Yes No Property
classFruit(var
name:String) Yes Yes Property
Sometimeswemaywonderwhenweshoulduseapropertyandwhenweshoulduseamethod.Agoodguidelinetofollowistousepropertyinsteadofmethodwhen:
ItdoesnotthrowanexceptionItischeaptocalculate(orcachedonthefirstrun)Itreturnsthesameresultovermultipleinvocations
ConstructorwithdefaultargumentsSincetheearlydaysofJava,therewasaseriousflawwithobjectcreation.Itisdifficulttocreateanobjectinstancewhenanobjectrequiresmultipleparametersandsomeofthoseparametersareoptional.Thereareafewwaystosolvethisproblem,suchas,theTelescopingconstructorpattern,theJavaBeanspattern,andeventheBuilderpattern.Eachofthemhavetheirprosandcons.
PatternsThepatternssolvetheissueofobjectcreation.Eachofthemisexplainedasfollows:
Telescopingconstructorpattern:Classwithalistofconstructorswhereeachoneaddsanewparameter.Nowadaysit'sconsideredananti-pattern,butAndroidframeworkstillusesitinafewplaces;forexample,theandroid.view.Viewclass:
valview1=View(context)
valview1=View(context,attributeSet)
valview1=View(context,attributeSet,defStyleAttr)
JavaBeanspattern:Parameterlessconstructorplusoneormoresettersmethodstoconfigureobjects.Themainproblemwiththispatternisthatwecan'tsaywhetherornotalltherequiredmethodshavebeencalledonanobject,soitmaybeonlypartiallyconstructed:
valanimal=Animal()
fruit.setWeight(10)
fruit.setSpeed(7.4)
fruit.setColor("Gray")
Builderpattern:Usesanotherobject,abuilder,thatreceivesinitializationargumentsstepbystepandthenreturnstheresultingconstructedobjectatoncewhenthebuildmethodiscalled;forexample,android.app.Notification.Builder,orandroid.app.AlertDialog.Builder:
Retrofitretrofit=newRetrofit.Builder()
.baseUrl("https://api.github.com/")
.build();
Foralongtime,builderwasmostwidelyused,butacombinationofdefaultargumentsandnamedargumentsyntaxisanevenmoreconciseoption.Let'sdefinesomedefaultvalue:
classFruit(weight:Int=0,fresh:Boolean=true,color:
String="Green")
Bydefiningdefaultparametervalues,wecancreateobjectsinmultipleways,
withoutaneedtopassallarguments:
valfruit=Fruit(7.4,false)
println(fruit.fresh)//prints:false
valfruit2=Fruit(7.4)
println(fruit.fresh)//prints:true
Usingargumentssyntaxwithdefaultparametersgivesusmuchmoreflexibilityinobjectscreation.Wecanpassonlyrequiredparametersinanyorderthatwewantwithoutdefiningmultiplemethodsandconstructors,asinthefollowingexample:
valfruit1=Fruit(weight=7.4,fresh=true,color="Yellow")
valfruit2=Fruit(color="Yellow")
InheritanceAswealreadyknow,asupertypeofallKotlintypesisAny.ItistheequivalentoftheJavaObjecttype.EachKotlinclassexplicitlyorimplicitlyextendstheAnyclass.Ifwedonotspecifytheparentclass,theAnywillbeusedimplicitlysetasparentfortheclass:
classPlant//ImplicitlyextendsAny
classPlant:Any//ExplicitlyextendsAny
Kotlin,likeJava,promotessingleinheritance,soaclasscanhaveonlyoneparentclass,butitcanimplementmultipleinterfaces.
IncontrasttoJava,everyclassandeverymethodinKotlinisfinalbydefault.ThisplaysalongwiththeEffectiveJavaItem17:Designanddocumentforinheritanceorelseprohibititrule.Thisisusedtopreventunexpectedbehaviorfromasubclassaltering.Modificationofabaseclasscancausetheincorrectbehaviorofsubclasses,becausethechangedcodeofthebaseclassnolongermatchestheassumptionsinitssubclasses.
Thismeansthataclasscannotbeextendedandamethodcannotbeoverriddenuntilit'sexplicitlydeclaredasopenusingtheopenkeyword.ThisistheexactoppositeoftheJavafinalkeyword.
Let'ssaywewanttodeclareabaseclassPlantandsubclassTree:
classPlant
classTree:Plant()//Error
Theprecedingcodewillnotcompile,becausetheclassPlantisfinalbydefault.Let'smakeitopen:
openclassPlant
classTree:Plant()
NoticethatwedefineinheritanceinKotlinsimplybyusingthecoloncharacter(:).ThereisnoextendsorimplementskeywordsknownfromJava.
Nowlet'saddsomemethodsandpropertiestoourPlantclass,andtrytooverride
itintheTreeclass:
openclassPlant{
varheight:Int=0
fungrow(height:Int){}
}
classTree:Plant(){
overridefungrow(height:Int){//Error
this.height+=height
}
}
Thiscodewillalsonotcompile.Wehavesaidalreadythatallmethodsarealsoclosedbydefault,soeachmethodwewanttooverridemustbeexplicitlymarkedasopen.Let'sfixthecodebymarkingthegrowmethodasopen:
openclassPlant{
varheight:Int=0
openfungrow(height:Int){}
}
classTree:Plant(){
overridefungrow(height:Int){
this.height+=height
}
}
Inasimilarway,wecouldopenandoverridetheheightproperty:
openclassPlant{
openvarheight:Int=0
openfungrow(height:Int){}
}
classTree:Plant(){
overridevarheight:Int=super.height
get()=super.height
set(value){field=value}
overridefungrow(height:Int){
this.height+=height
}
}
Toquicklyoverrideanymember,gotoaclasswhereamemberisdeclared,addtheopenmodifier,andthengotoaclasswherewewanttooverridemember,runtheoverridemembers(theshortcutforWindowsisCtrl+O,andformacOS,itisCommand+O)action,andselectallthemembersyouwanttooverride.ThiswayalltherequiredcodewillbegeneratedbyAndroidStudio.
Let'sassumethatalltreesgrowinthesameway(thesamecomputationof
growingalgorithmisapplicableforalltrees).WewanttoallowcreatingnewsubclassesoftheTreeclasstohavemorecontrolovertrees,butatthesametimewewanttopreserveourgrowingalgorithm--notallowinganysubclassesoftheTreeclasstooverridethisbehavior.Toachievethis,weneedtoexplicitlymarkthegrowmethodintheTreeclassasfinal:
openclassPlant{
varheight:Int=0
openfungrow(height:Int){}
}
classTree:Plant(){
finaloverridefungrow(height:Int){
this.height+=height
}
}
classOak:Tree(){
//1
}
1. It'snotpossibletooverridegrowmethodherebecauseit'sfinal
Let'ssumupallthisopenandfinalbehavior.Tomakeamethodoverridableinasubclass,weneededtoexplicitlymarkitasopeninthesuperclass.Tomakesurethatoverriddenmethodwillnotbeoverriddenagainbyanysubclass,weneedtomarkitasfinal.
Intheprecedingexample,thegrowmethodinthePlantclassdoesnotreallyprovideanyfunctionality(ithasanemptybody).Thisisasignthatmaybewedon'twanttoinstantiatethePlantclassatall,buttreatitasabaseclassandonlyinstantiatevariousclassessuchasTreethatextendsthePlantclass.WeshouldmarkthePlantclassasabstracttodisallowitsinstantiation:
abstractclassPlant{
varheight:Int=0
abstractfungrow(height:Int)
}
classTree:Plant(){
overridefungrow(height:Int){
this.height+=height
}
}
valplant=Plant()
//error:abstractclasscan'tbeinstantiated
valtree=Tree()
Markingtheclassasabstractwillalsomakethemethodclassopenbydefault,sowedon'thavetoexplicitlymarkeachmemberasopen.Noticethatwhenwearedefiningthegrowmethodasabstract,wehavetoremoveitsbody,becausetheabstractmethodcan'thaveabody.
TheJvmOverloadsannotationSomeclassesintheAndroidplatformuseTelescopingconstructors,whichisconsideredasananti-pattern.Agoodexampleofsuchaclassistheandroid.view.Viewclass.Theremaybeacasewhenonlyasingleconstructorisused(inflatingthecustomviewfromKotlincode),butitismuchsafertooverrideallthreeconstructorswhenthesubclassingsubclassandroid.view.View,becausetheclasswillworkcorrectlyinallscenarios.Normallyourcustomviewclasswouldlooklikethis:
classCustomView:View{
constructor(context:Context?):this(context,null)
constructor(context:Context?,attrs:AttributeSet?):
this(context,attrs,0)
constructor(context:Context?,attrs:AttributeSet?,defStyleAttr:Int):super(context,attrs,defStyleAttr){
//...
}
}
Thiscaseintroducesalotofboilerplatecodejustforconstructorsthatdelegatecallstootherconstructors.Kotlin'ssolutiontothisproblemistousethe@JvmOverloadannotation:
classKotlinView@JvmOverloadsconstructor(
context:Context,
attrs:AttributeSet?=null,
defStyleAttr:Int=0
):View(context,attrs,defStyleAttr)
Annotatingaconstructorwiththe@JvmOverloadannotationinformsthecompilertogenerateinJVMbytecodeadditionalconstructoroverloadforeveryparameterwithadefaultvalue.Inthiscase,alltherequiredconstructorswillbegenerated:
publicSampleView(Contextcontext){
super(context);
}
publicSampleView(Contextcontext,@NullableAttributeSetattrs){
super(context,attrs);
}
publicSampleView(Contextcontext,@NullableAttributeSetattrs,intdefStyleAttr){
super(context,attrs,defStyleAttr);
}
InterfacesKotlininterfacesaresimilartoJava8interfacesandincontrasttointerfacesfrompreviousJavaversions.Aninterfaceisdefinedusingtheinterfacekeyword.Let'sdefineanEmailProviderinterface:
interfaceEmailProvider{
funvalidateEmail()
}
ToimplementtheprecedinginterfaceinKotlin,usethesamesyntaxasforextendingclasses--asinglecoloncharacter(:).ThereisnoimplementskeywordlikeinJava:
classUser:EmailProvider{
overridefunvalidateEmail(){
//emailvalidation
}
}
Thequestionmayariseofhowtoextendaclassandimplementaninterfaceatthesametime.Simplyplacetheclassnameafterthecolon,andusecommacharactertoaddoneormoreinterfaces.It'snotrequiredtoplacethesuperclassatthefirstpositionalthoughit'sconsideredgoodpractice:
openclassPerson{
interfaceEmailProvider{
funvalidateEmail()
}
classUser:Person(),EmailProvider{
overridefunvalidateEmail(){
//emailvalidation
}
}
AswithJava,theKotlinclasscanextendonlyoneclass,butitcanimplementoneormoreinterfaces.Wecanalsodeclarepropertiesintheinterfaces:
interfaceEmailProvider{
valemail:String
funvalidateEmail()
}
Allmethodsandpropertieshavetobeoverriddeninaclassimplementing
interface:
classUser():EmailProvider{
overridevalemail:String="UserEmailProvider"
overridefunvalidateEmail(){
//emailvalidation
}
}
Also,propertiesdefinedinaprimaryconstructorcanbeusedtooverrideparametersfromaninterface:
classUser(overridevalemail:String):EmailProvider{
overridefunvalidateEmail(){
//emailvalidation
}
}
Allmethodsandpropertiesdefinedinaninterfacethatdoesnothavedefaultimplementationaretreatedbydefaultasabstract,sowedon'thavetoexplicitlydefinethemasabstract.Allabstractmethodsandpropertiesmustbeimplemented(overridden)byaconcrete(non-abstract)classthatimplementsaninterface.
Thereis,however,anotherwaytodefinemethodsandpropertiesintheinterface.Kotlin,similartoJava8,introducesmajorimprovementtointerfaces.Aninterfacecannotonlydefinebehavior,butalsoimplementsit.Thismeansthatthedefaultmethodofpropertyimplementationcanbeprovidedbyaninterface.Theonlylimitationisthataninterfacecannotreferenceanybackingfields--storeastate(becausethereisnogoodplacetostoreit).Thisisadifferingfactorbetweeninterfaceandabstractclass.Interfacesarestateless(theycan'thaveastate),whileabstractclassesarestateful(theycanhaveastate).Let'sseeanexample:
interfaceEmailProvider{
funvalidateEmail():Boolean
valemail:String
valnickname:String
get()=email.substringBefore("@")
}
classUser(overridevalemail:String):EmailProvider{
overridefunvalidateEmail(){
//emailvalidation
}
}
TheEmailProviderinterfaceprovidesthedefaultimplementationforthenicknameproperty,sowedon'thavetodefineitintheUserclass,andwecanstillusethepropertyasanyotherpropertydefinedintheclass:
valuser=User("johnny.bravo@test.com")
print(user.nickname)//prints:johnny
Thesameappliesformethods.Simplydefineamethodwiththebodyintheinterface,sotheUserclasswilltakealldefaultimplementationfromtheinterface,andwillhavetooverrideonlytheemailmember--theonlymemberintheinferencewithoutdefaultimplementation:
interfaceEmailProvider{
valemail:String
valnickname:String
get()=email.substringBefore("@")
funvalidateEmail()=nickname.isNotEmpty()
}
classUser(overridevalemail:String):EmailProvider
//usage
valuser=User("joey@test.com")
print(user.validateEmail())//Prints:true
print(user.nickname)//Prints:joey
Thereisoneinterestingcaserelatedtodefaultimplementations.Aclasscan'tinheritfrommultipleclasses,butitcanimplementmultipleinterfaces.Wecanhavetwointerfacescontainingmethodswiththesamesignatureanddefaultimplementations:
interfaceA{
funfoo(){
println("A")
}
}
interfaceB{
funfoo(){
println("B")
}
}
Insuchcases,conflictmustberesolvedexplicitlybyoverridingthefoomethodinaclassimplementingtheinterfaces:
classItem:A,B{
overridefunfoo(){
println("Item")
}
}
//usage
valitem=Item()
item.foo()//prints:Item
Wecanstillcallbothdefaultinterfaceimplementationsbyqualifyingsuperusinganglebracketsandspecifyingtheparentinterfacetypename:
classItem:A,B{
overridefunfoo(){
vala=super<A>.foo()
valb=super<B>.foo()
print("Item$a$b")
}
}
//usage
valitem=Item()
item.foo()
//Prints:A
B
ItemsAB
DataclassesOftenwecreateaclasswhoseonlypurposeistostoredata;forexample,dataretrievedfromaserverorlocaldatabase.Thoseclassesarebuildingblocksofapplicationdatamodels:
classProduct(varname:String?,varprice:Double?){
overridefunhashCode():Int{
varresult=if(name!=null)name!!.hashCode()else0
result=31*result+if(price!=null)price!!.hashCode()
else0
returnresult
}
overridefunequals(other:Any?):Boolean=when{
this===other->true
other==null||other!isProduct->false
if(name!=null)name!=other.nameelseother.name!=
null->false
price!=null->price==other.price
else->other.price==null
}
overridefuntoString():String{
return"Product(name=$name,price=$price)"
}
}
InJava,weneedtogeneratealotofredundantgetters/setterstogetherwithhashCodeandequalsmethods.AndroidStudiocangeneratemostofthecodeforus,butmaintainingthiscodeisstillanissue.InKotlin,wecandefineaspecialkindofclasscalledthedataclassbyaddingthedatakeywordtoaclassdeclarationheader:
classProduct(varname:String,varprice:Double)
//normalclass
dataclassProduct(varname:String,varprice:Double)
//dataclass
AdataclassaddsadditionalcapabilitiestoaclassintheformofmethodsgeneratedbytheKotlincompiler.Thosemethodsareequals,hashCode,toString,copy,andmultiplecomponentNmethods.Thelimitationisthatdataclassescan'tbemarkedasabstract,inner,andsealed.Let'sdiscussmethodsaddedbyadatamodifierinmoredetail.
TheequalsandhashCodemethodWhendealingwithdataclasses,thereisoftenaneedtocomparetwoinstancesforstructuralequality(thattheycontainthesamedata,butnotnecessarilyarethesameinstance).WemanysimplywanttocheckifoneinstanceoftheUserclassequalsanotherUserinstanceoriftwoproductinstancesrepresentthesameproduct.AcommonpatternusedtocheckifobjectsareequalistouseanequalsmethodthatusesthehashCodemethodinternally:
product.equals(product2)
ThegeneralcontractforoverriddenimplementationsofhashCodeisthattwoequalobjects(accordingtoequalsimplementation)needtohavethesamehashcode.ThereasonbehinditisthathashCodeisoftencomparedbeforeequals,becauseofitsperformance--it'smuchcheapertocomparehashcodethaneveryfieldintheobject.
IfhashCodeisthesamethentheequalsmethodchecksiftwoobjectsarethesameinstance,thesametype,andthenverifiesequalitybycomparingallsignificantfields.Ifatleastoneofthefieldsofthefirstobjectisnotequaltoacorrespondingfieldofasecondobjectthentheobjectsarenotconsideredasequal.Anotherwayaround--twoobjectsareequalwhentheyhavethesamehashCodeandallsignificant(compared)fieldshavethesamevalue.Let'scheckanexampleoftheJavaproductclasscontainingtwofields,nameandprice:
publicclassProduct{
privateStringname;
privateDoubleprice;
publicProduct(Stringname,Doubleprice){
this.name=name;
this.price=price;
}
@Override
publicinthashCode(){
intresult=name!=null?name.hashCode():0;
result=31*result+(price!=null?
price.hashCode():0);
returnresult;
}
@Override
publicbooleanequals(Objecto){
if(this==o){
returntrue;
}
if(o==null||getClass()!=o.getClass()){
returnfalse;
}
Productproduct=(Product)o;
if(name!=null?!name.equals(product.name):
product.name!=null){
returnfalse;
}
returnprice!=null?price.equals(product.price):
product.price==null;
}
publicStringgetName(){
returnname;
}
publicvoidsetName(Stringname){
this.name=name;
}
publicDoublegetPrice(){
returnprice;
}
publicvoidsetPrice(Doubleprice){
this.price=price;
}
}
ThisapproachiswidelyusedinJavaandotherOOPprogramminglanguages.Intheearlydays,programmershadtowritethiscodemanuallyforeveryclassthatneededtobecomparedandmaintainedthecodemakingsurethatitwascorrectanditcompareseverysignificantvalue.
Nowadays,modernIDEssuchasAndroidStudiocangeneratethiscodeandupdatetheappropriatemethods.Wedon'thavetowritethecode,butwestillhavetomaintainitbymakingsurethatalltherequiredfieldsarecomparedbytheequalsmethod.Sometimeswedon'tknowifitisastandardcodegeneratedbytheIDEoritisatweakedversion.ForeachKotlindataclass,thosemethodsareautomaticallygeneratedbyacompiler,sothisproblemdoesnotexist.HereisadefinitionofProductinKotlin,whichcontainsallmethodsdefinedinthepreviousJavaclasses:
dataclassProduct(varname:String,varprice:Double)
TheprecedingclasscontainsallmethodsdefinedinpreviousJavaclasses,but
thereisnomassiveboilerplatecodetomaintain.
InChapter2,LayingaFoundation,wementionedthat,inKotlin,usingthestructuralequalityoperator(==)willalwayscalltheequalsmethodunderthehood,soitmeansthatwecaneasilyandsafelycompareinstancesofourProductdataclass:
dataclassProduct(varname:String,varprice:Double)
valproductA=Product("Spoon",30.2)
valproductB=Product("Spoon",30.2)
valproductC=Product("Fork",17.4)
print(productA==productA)//prints:true
print(productA==productB)//prints:true
print(productB==productA)//prints:true
print(productA==productC)//prints:false
print(productB==productC)//prints:false
Bydefault,thehashCodeandequalsmethodsaregeneratedbasedoneverypropertydeclaredintheprimaryconstructor.Inmostscenariosthisisenough,butifweneedmorecontrolwearestillallowedtooverridethesemethodsbyourselvesinthedataclass.Inthiscase,thedefaultimplementationwon'tbegeneratedbythecompiler.
ThetoStringmethodGeneratedmethodscontainnamesandvaluesofallpropertiesdeclaredintheprimaryconstructor:
dataclassProduct(varname:String,varprice:Double)
valproductA=Product("Spoon",30.2)
println(productA)//prints:Product(name=Spoon,price=30.2)
Wecanactuallylogmeaningfuldatatoaconsoleorlogfile,insteadofclassnameandmemoryaddresslikeinJava(Person@a4d2e77).Thismakesthedebuggingprocessmuchsimpler,becausewehaveaproper,humanreadableformat.
ThecopymethodBydefault,theKotlincompilerwillalsogenerateanappropriatecopymethodthatwillallowustoeasilycreateacopyofanobject:
dataclassProduct(varname:String,varprice:Double)
valproductA=Product("Spoon",30.2)
print(productA)//prints:Product(name=Spoon,price=30.2)
valproductB=productA.copy()
print(productB)//prints:Product(name=Spoon,price=30.2)
Javadoesnothavenamedargumentsyntax,sowhencallingthecopymethodJavacodeweneedtopassallarguments(theorderoftheargumentscorrespondstotheorderofpropertiesdefinedintheprimaryconstructor).InKotlin,thisapproachdecreasestheneedforcopyconstructorsorcopyfactories:
ThecopyconstructortakesasingleargumentandtypeistheclasscontainingtheconstructorandreturnsthenewInstanceofthisclass:
valproductB=Product(productA)
Thecopyfactoryisthestaticfactorythattakesasingleargumentwhosetypeistheclasscontainingthefactoryandreturnsanewinstanceofthisclass:
valproductB=ProductFactory.newInstance(productA)
Thecopymethodtakesargumentsthatcorrespondtoallpropertiesdeclaredintheprimaryconstructor.Whencombinedwiththedefaultargumentssyntax,wecanprovidealloronlysomeofthepropertiestocreateamodifiedinstancecopy:
dataclassProduct(varname:String,varprice:Double)
valproductA=Product("Spoon",30.2)
print(productA)//prints:Product(name=Spoon,price=30.2)
valproductB=productA.copy(price=24.0)
print(productB)//prints:Product(name=Spoon,price=24.0)
valproductC=productA.copy(price=24.0,name="Knife")
print(productB)//prints:Product(name=Knife,price=24.0)
Thisisaveryflexiblewayofcreatingcopyoftheobjectwherewecaneasily
sayif,andhow,copyshoulddifferfromoriginalinstances.Ontheotherhand,theprogrammingapproachpromotesconceptofimmutability,whichcanbeeasilyimplementedwithanargumentlesscallofthecopymethod:
//Mutableobject-modifyobjectstate
dataclassProduct(varname:String,varprice:Double)
varproductA=Product("Spoon",30.2)
productA.name="Knife"
//immutableobject-createnewobjectinstance
dataclassProduct(valname:String,valprice:Double)
varproductA=Product("Spoon",30.2)
productA=productA.copy(name="Knife")
Insteadofdefiningmutableproperties(var)andmodifyingtheobjectstate,wecandefineimmutableproperties(val),makeanobjectimmutable,andoperateonitbygettingitscopywiththechangedvalues.Thisapproachreducestheneedofdatasynchronizationinmultithreadingapplicationsandthenumberofpotentialerrorsrelatedwithit,becauseimmutableobjectscanbefreelysharedacrossthreads.
DestructivedeclarationsSometimesitmakessensetorestructureobjectsintomultiplevariables.Thissyntaxiscalledadestructuringdeclaration:
dataclassPerson(valfirstName:String,vallastName:String,
valheight:Int)
valperson=Person("Igor","Wojda",180)
var(firstName,lastName,height)=person
println(firstName)//prints:"Igor"
println(lastName)//prints:"Wojda"
println(height)//prints:180
Adestructuringdeclarationallowsustocreatemultiplevariablesatonce.TheprecedingcodewillresultincreatingvaluesthefirstName,lastName,andheightvariables.Underthehood,thecompilerwillgeneratecodelikethis:
valperson=Person("Igor","Wojda",180)
varfirstName=person.component1()
varlastName=person.component2()
varheight=person.component3()
Foreverypropertydeclaredintheprimaryconstructorofthedataclass,theKotlincompilergeneratesasinglecomponentNmethod.Thesuffixofthecomponentfunctioncorrespondstotheorderofpropertiesdeclaredintheprimaryconstructor,sothefirstNamecorrespondstocomponent1,lastNamecorrespondstocomponent2,andheightcorrespondstocomponent3.Infact,wecouldinvokethosemethodsdirectlyonthePersonclasstoretrieveapropertyvalue,butthereisnopointofdoingso,becausetheirnamesaremeaninglessandcodewouldbeverydifficulttoreadandmaintain.Weshouldleavethosemethodsforthecompilerfordestructuringtheobjectandusepropertyaccesssyntaxsuchasperson.firstName.
Wecanalsoomitoneormorepropertiesusinganunderscore:
valperson=Person("Igor","Wojda",180)
var(firstName,_,height)=person
println(firstName)//prints:"Igor"
println(height)//prints:180
Inthiscase,wewantonlytocreatetwovariables,firstNameandheight;thelastNameisignored.Thecodegeneratedbythecompilerwilllookasfollows:
valperson=Person("Igor","Wojda",180)
varfirstName=person.component1()
varheight=person.component3()
WecanalsodestructuresimpletypeslikeString:
valfile="MainActivity.kt"
val(name,extension)=file.split(".",limit=2)
Destructivedeclarationscanalsobeusedtogetherwiththeforloop:
valauthors=listOf(
Person("Igor","Wojda",180),
Person("Marcin","Moskała",180)
)
println("Authors:")
for((name,surname)inauthors){
println("$name$surname")
}
OperatoroverloadingKotlinhasapredefinedsetofoperatorswithfixedsymbolicrepresentation(+,*,andsoon)andfixedprecedence.Mostoftheoperatorsaretranslateddirectlyintomethodcalls;somearetranslatedintomorecomplexexpressions.ThefollowingtablecontainsalistofalltheoperatorsavailableinKotlin:
Operatortoken Correspondingmethod/expression
a+b a.plus(b)
a-b a.minus(b)
a*b a.times(b)
a/b a.div(b)
a%b a.rem(b)
a..b a.rangeTo(b)
a+=b a.plusAssign(b)
a-=b a.minusAssign(b)
a*=b a.timesAssign(b)
a/=b a.divAssign(b)
a%=b a.remAssign(b)
a++ a.inc()
a-- a.dec()
ainb b.contains(a)
a!inb !b.contains(a)
a[i] a.get(i)
a[i,j] a.get(i,j)
a[i_1,...,i_n] a.get(i_1,...,i_n)
a[i]=b a.set(i,b)
a[i,j]=b a.set(i,j,b)
a[i_1,...,i_n]=b a.set(i_1,...,i_n,b)
a() a.invoke()
a(i) a.invoke(i)
a(i,j) a.invoke(i,j)
a(i_1,...,i_n) a.invoke(i_1,...,i_n)
a==b a?.equals(b)?:(b===null)
a!=b !(a?.equals(b)?:(b===null))
a>b a.compareTo(b)>0
a<b a.compareTo(b)<0
a>=b a.compareTo(b)>=0
a<=b a.compareTo(b)<=0
TheKotlincompilertranslatestokensthatrepresentspecificoperations(leftcolumn)tocorrespondingmethodsorexpressionsthatwillbeinvoked(rightcolumn).
Wecanprovidecustomimplementationsforeachoperatorbyusingtheminclassoperatormethodcorrespondingwithanoperatortoken.Let'sdefineasimplePointclasscontainingxandypropertiestogetherwithtwooperators,plusandtimes:
dataclassPoint(varx:Double,vary:Double){
operatorfunplus(point:Point)=Point(x+point.x,y+point.y)
operatorfuntimes(other:Int)=Point(x*other,y*other)
}
//usage
varp1=Point(2.9,5.0)
varp2=Point(2.0,7.5)
println(p1+p2)//prints:Point(x=4.9,y=12.5)
println(p1*3)//prints:Point(x=8.7,y=21.0)
Bydefiningplusandtimesoperators,wecanperformadditionandmultiplicationoperationsonanyPointinstance.Eachtime+or*operationsarecalled,Kotlincallscorrespondingoperatormethodplusortimes.Underthehood,thecompilerwillgeneratemethodcalls:
p1.plus(p2)
p1.times(3)
Inourexample,wearepassingtheotherpointinstancetotheplusoperatormethod,butthistypeisnotmandatory.Operatormethoddoesnotactuallyoverrideanymethodfromthesuperclass,soithasnofixeddeclarationwithfixedparametersandfixedtypes.Wedon'thavetoinheritfromaparticularKotlintypetobeabletooverloadoperators.Allweneedtohaveisamethodwithpropersignaturemarkedasoperator.TheKotlincompilerwilldotherestbytherunningmethodthatcorrespondstotheoperator.Infact,wecandefinemultipleoperatorswiththesamenameanddifferentparametertypes:
dataclassPoint(varx:Double,vary:Double){
operatorfunplus(point:Point)=Point(x+point.x,y+point.y)
operatorfunplus(vector:Double)=Point(x+vector,y+vector)
}
varp1=Point(2.9,5.0)
varp2=Point(2.0,7.5)
println(p1+p2)//prints:Point(x=4.9,y=12.5)
println(p1+3.1)//prints:Point(x=6.0,y=10.1)
Bothoperatorsareworkingfine,becausetheKotlincompilercanselectproperoverloadoftheoperator.Manybasicoperatorshavecorrespondingcompoundassignoperator(plushasplusAssign,timeshastimesAssign,andsoon),sowhenwedefineanoperatorsuchasthe+operator,Kotlinsupportsthe+operationand+=operationaswell:
varp1=Point(2.9,7.0)
varp2=Point(2.0,7.5)
p1+=p2
println(p1)//prints:Point(x=4.9,y=14.5)
Noticetheimportantdifferencethatinsomescenariositmaybeperformancecritical.Acompoundassignoperator(forexample,the+=operator)hastheUnitreturntype,soitjustmodifiesthestateoftheexistingobject,whilethebasicoperator(forexample,the+operator)alwaysreturnsanewinstanceofanobject:
varp1=Wallet(39.0,14.5)
p1+=p2//updatestateofp1
valp3=p1+p2//createsnewobjectp3
WhenwedefinebothplusandplusAssignoperatorswiththesameparametertypes,whenwetrytousetheplusAssign(compound)operator,thecompilerwillthrow
anerror,becauseitdoesnotknowwhichmethodshouldbeinvoked:
dataclassPoint(varx:Double,vary:Double){
init{
println("Pointcreated$x.$y")
}
operatorfunplus(point:Point)=Point(x+point.x,y+point.y)
operatorfunplusAssign(point:Point){
x+=point.x
y+=point.y
}
}
\\usage
varp1=Point(2.9,7.0)
varp2=Point(2.0,7.5)
valp3=p1+p2
p1+=p2//Error:Assignmentoperationsambiguity
OperatoroverloadingworksalsoforclassesdefinedinJava.Allweneedisamethodwiththepropersignatureandnamethatcorrespondstotheoperator'smethodname.TheKotlincompilerwilltranslateoperatorusagetothismethod.OperatormodifierisnotpresentinJava,soit'snotrequiredintheJavaclass:
//Java
publicclassPoint{
privatefinalintx;
privatefinalinty;
publicPoint(intx,inty){
this.x=x;
this.y=y;
}
publicintgetX(){
returnx;
}
publicintgetY(){
returny;
}
publicPointplus(Pointpoint){
returnnewPoint(point.getX()+x,point.getY()+y);
}
}
//Main.kt
valp1=Point(1,2)
valp2=Point(3,4)
valp3=p1+p2;
println("$x:{p3.x},y:${p3.y}")//prints:x:4,y:6
ObjectdeclarationThereareafewwaystodeclaresingletonsinJava.Hereisthemostcommonwaytodefinetheclassthathasaprivateconstructorandretrievesinstancesviaastaticfactorymethod:
publicclassSingleton{
privateSingleton(){
}
privatestaticSingletoninstance;
publicstaticSingletongetInstance(){
if(instance==null){
instance=newSingleton();
}
returninstance;
}
}
Theprecedingcodeworksfineforasinglethread,butit'snotthreadsafe,soinsomecasestwoinstancesofSingletoncanbecreated.Thereareafewwaystofixit.Wecanusethesynchronizedblockpresentedasfollows:
//synchronized
publicclassSingleton{
privatestaticSingletoninstance=null;
privateSingleton(){
}
privatesynchronizedstaticvoidcreateInstance(){
if(instance==null){
instance=newSingleton();
}
}
publicstaticSingletongetInstance(){
if(instance==null)createInstance();
returninstance;
}
}
Thissolution,however,isveryverbose.InKotlin,thereisaspeciallanguageconstructforcreatingsingletonscalledobjectdeclaration,sowecanachievethesameresultinamuchsimplerway.Definingobjectsissimilartodefiningclasses;theonlydifferenceisthatweusetheobjectkeywordinsteadoftheclass
keyword:
objectSingleton
Wecanaddmethodsandpropertiestoanobjectdeclarationexactlythesamewayasinaclass:
objectSQLiteSingleton{
fungetAllUsers():List<User>{
//...
}
}
ThismethodisaccessedthesamewayasanyJavastaticmethod:
SQLiteSingleton.getAllUsers()
Objectdeclarationsareinitializedlazilyandtheycanbenestedinsideotherobjectdeclarationsornon-innerclasses.Also,theycannotbeassignedtoavariable.
ObjectexpressionAnobjectexpressionisequivalenttoJava'sanonymousclass.Itisusedtoinstantiateobjectsthatmightinheritfromsomeclassorimplementsaninterface.Aclassicuse-caseiswhenweneedtodefineobjectsthatareimplementingsomeinterface.ThisishowinJavawecouldimplementtheServiceConnectioninterfaceandassignittoavariable:
ServiceConnectionserviceConnection=newServiceConnection(){
@Override
publicvoidonServiceDisconnected(ComponentNamename){
...
}
@Override
publicvoidonServiceConnected(ComponentNamename,
IBinderservice)
{
...
}
}
TheclosestKotlinequivalentoftheprecedingimplementationisthefollowing:
valserviceConnection=object:ServiceConnection{
overridefunonServiceDisconnected(name:ComponentName?){}
overridefunonServiceConnected(name:ComponentName?,
service:IBinder?){}
}
Theprecedingexampleisusinganobjectexpression,whichcreatesinstanceofanonymousclassthatimplementsServiceConnectioninterface.Anobjectexpressioncanalsoextendclasses.HereishowwecancreateaninstanceoftheabstractclassBroadcastReceiver:
valbroadcastReceiver=object:BroadcastReceiver(){
overridefunonReceive(context:Context,intent:Intent){
println("Gotabroadcast${intent.action}")
}
}
valintentFilter=IntentFilter("SomeAction");
registerReceiver(broadcastReceiver,intentFilter)
Whileobjectexpressionsallowustocreateobjectsofananonymoustypethat
canimplementsomeinterfaceandextendsomeclass,wecanusethemtoeasilysolveinterestingproblemsrelatedtotheAdapterpattern.
TheAdapterdesignpatternallowsotherwiseincompatibleclassestoworktogetherbyconvertingtheinterfaceofoneclassintoaninterfaceexpectedbytheclients.
Let'ssaythatwehaveaPlayerinterfaceandfunctionthatrequiresPlayerasaparameter:
interfacePlayer{
funplay()
}
funplayWith(player:Player){
print("Iplaywith")
player.play()
}
Also,wehaveVideoPlayerclassfromapubliclibrarythathastheplaymethoddefined,butitisnotimplementingourPlayerinterface:
openclassVideoPlayer{
funplay(){
println("Playvideo")
}
}
TheVideoPlayerclassmeetsalltheinterfacerequirements,butitcannotbepassedasPlayerbecauseitisnotimplementingtheinterface.Touseitasaplayer,weneedtomakeanAdapter.Inthisexample,wewillimplementitasanobjectofananonymoustypethatimplementsthePlayerinterface:
valplayer=object:VideoPlayer(),Player{}
playWith(player)
WewereabletosolveourproblemwithoutdefiningtheVideoPlayersubclass.Wecanalsodefinecustommethodsandpropertiesintheobjectexpression:
valdata=object{
varsize=1
funupdate(){
//...
}
}
data.size=2
data.update()
ThisisaveryeasywaytodefinecustomanonymousobjectsthatarenotpresentinJava.TodefinesimilartypesinJava,weneedtodefinethecustominterface.WecannowaddabehaviortoourVideoPlayerclasstofullyimplementthePlayerinterface:
openclassVideoPlayer{
funplay(){
println("Playvideo")
}
}
interfacePlayer{
funplay()
funstop()
}
//usage
valplayer=object:VideoPlayer(),Player{
varduration:Double=0.0
funstop(){
println("Stopvideo")
}
}
player.play()//println("Playvideo")
player.stop()//println("Stopvideo")
player.duration=12.5
Intheprecedingcode,wecancallonanonymousobject(player)methodsdefinedintheVideoPlayerclassandexpressionobject.
CompanionobjectsKotlin,asopposedtoJava,lackstheabilitytodefinestaticmembers,butinsteaditallowsustodefineobjectsthatareassociatedwithaclass.Inotherwords,anobjectisinitializedonlyonce;thereforeonlyoneinstanceofanobjectexists,sharingitsstateacrossallinstancesofaparticularclass.Whenasingletonobjectisassociatedwithaclassofthesamename,itiscalledthecompanionobjectoftheclass,andtheclassiscalledthecompanionclassoftheobject:
TheprecedingdiagrampresentsthreeinstancesoftheCarclasssharingasingleinstanceofanobject.
Members,suchasmethodsandproperties,definedinsideacompanionobjectmaybeaccessedsimilarlytothewayweaccessstaticfieldsandmethodsinJava.Themainpurposeofacompanionobjectistohavecodethatisrelatedtoclass,butnotnecessarytoanyparticularinstanceofthisclass.ItisagoodwaytodefinemembersthatwouldbedefinedasstaticinJava;forexample,factory,whichcreatesaclassinstancemethodconvertingsomeunits,activityrequestcode,sharedpreferenceskey,andsoon.Todefinethesimplestcompanionobject,weneedtodefineasingleblockofcode:
classProductDetailsActivity{
companionobject{
}
}
Nowlet'sdefineastartmethodthatwillallowustostartanactivityinaneasyway:
//ProductDetailsActivity.kt
classProductDetailsActivity:AppCompatActivity(){
overridefunonCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
valproduct=intent.getParcelableExtra<Product>
(KEY_PRODUCT)//3
//...
}
companionobject{
constvalKEY_PRODUCT="product"//1
funstart(context:Context,product:Product){//2
valintent=Intent(context,
ProductDetailsActivity::class.java)
intent.putExtra(KEY_PRODUCT,product)//3
context.startActivity(intent)
}
}
}
//Startactivity
ViewProductActivity.start(context,productId)//2
1. Onlysingleinstanceofkeyexists2. Methodstartcanbeinvokedwithoutcreatingobjectinstance.JustlikeJava
staticmethod.3. Retreivevalueafterinstanceiscreated.
Noticethatweareabletocallstartpriortotheactivityinstancecreation.Let'susethecompanionobjecttotrackhowmanyinstancesoftheCarclasswerecreated.Toachievethisweneedtodefinethatcountpropertywithaprivatesetter.Itcouldbealsodefinedasatop-levelproperty,butitisbettertoplaceitinsideacompanionobject,becausewedon'twanttoallowcountermodificationoutsideofthisclass:
classCar{
init{
count++;
}
companionobject{
varcount:Int=0
privateset
}
}
Theclasscanaccessallthemethodsandpropertiesdefinedinthecompanionobject,butthecompanionobjectcan'taccessanyoftheclasscontent.Thecompanionobjectisassignedtoaspecificclass,butnottoaparticularinstance:
println(Car.count)//Prints0
Car()
Car()
println(Car.count)//Prints:2
Toaccessaninstanceofthecompanionobjectdirectly,wecanusetheclassname.
Wecanalsoaccessthecompanionobjectbyusingmoreverbosesyntax,Car.Companion.count,butinmostcasesthereisnopointofdoingso,unlesswewanttoaccesscompanionfromJavacode.
CompanionobjectinstantiationAcompanionobjectisasingletoncreatedbyacompanionclassandkeptinitsstaticproperty.Theinstantiationofacompanionobjectislazy.Thismeansthatcompanionobjectwillbeinstantiatedwhenitisneededforthefirsttime--whenitsmembersareaccessed,orinstanceofaclasscontainingthecompanionobjectiscreated.TomarkupwhentheCarclassinstanceanditscorrespondingcompanionobjectarecreated,weneedtoaddtwoinitializerblocks--onefortheCarclass,anotherforthecompanionobject.
Theinitializerblockinsidethecompanionobjectworksexactlythesamewayasintheclass--it'sexecutedwhenaninstanceiscreated:
classCar{
init{
count++;
println("Carcreated")
}
companionobject{
varcount:Int=0
init{
println("Carcompanionobjectcreated")
}
}
}
WhiletheclassinitializationblockisequivalentoftheJavaconstructorbody,thecompilationobjectinitializationblockistheequivalentoftheJavastaticinitializationblockinKotlin.Fornow,thecountpropertycanbeupdatedbyanyclient,becauseit'saccessiblefromoutsideoftheCarclass.WewillfixthisissuelaterinthischapterintheVisibilitymodifierssection.Nowlet'saccesstheCarcompanionobjectclassmember:
Car.count//Prints:Carcompanionobjectcreated
Car()//Prints:Carcreated
Byaccessingthecountpropertydefinedinthecompanionobject,wetriggeritscreation,butnoticethataninstanceofCarclassisnotcreated.LaterwhenwecreateaCarclassinstancecompanionobjectisalreadycreated.Nowlet'sinstantiatetheCarclassbeforeaccessingthecompanionobject:
Car()
//Prints:Carcompanionobjectcreated
//Prints:Carcreated
Car()//Prints:Carcreated
Car.count
CompanionobjectiscreatedtogetherwithfirstinstanceofCarclass,sowhenwecreatesomeotherinstancesoftheuserclass,thecompanionobjectfortheclassalreadyexists,soit'snotcreated.
Keepinmindthattheprecedinginstantiationdescribestwoseparateexamples.Bothcouldnotbetrueinasingleprogram,becauseonlyasingleinstanceofclasscompanionobjectcanexistanditiscreatedthefirsttimewhenitisneeded.
Thecompanionobjectscanalsocontainfunctions,implementinterfaces,andevenextendclasses.Wecandefineacompanionobjectthatwillincludeastaticconstrictionmethodwiththeadditionalpossibilitytooverrideimplementationfortestingpurposes:
abstractclassProvider<T>{//1
abstractfuncreator():T//2
privatevarinstance:T?=null//3
varoverride:T?=null//4
funget():T=override?:instance?:creator().also{instance=it}//5
}
1. Providerisagenericclass.2. Abstractfunctionusedtocreateinstance.3. Fieldusedtokeepcreatedinstance.4. Fieldusedtointests,toprovidealternativeimplementationofinstance.5. Functionthatisreturningoverrideinstanceifitwasset,instanceifitwas
created,oritiscreatinginstanceusingthecreatemethodandfillinginstancefieldwithit.
Withsuchimplementation,wecandefinetheinterfacewithadefaultstaticconstructor:
interfaceMarvelRepository{
fungetAllCharacters(searchQuery:String?):Single<List<MarvelCharacter>>
companionobject:Provider<MarvelRepository>(){
overridefuncreator()=MarvelRepositoryImpl()
}
}
Togetinstance,weneedtouse:
MarvelRepository.get()
Ifweneedtospecifysomeotherinstancefortestingpurposes(forexample,inEspressotests)thenwecanalwaysspecifythemusingobjectexpression:
MarvelRepository.override=object:MarvelRepository{
overridefungetAllCharacters(searchQuery:String?):
Single<List<MarvelCharacter>>{
//...
}
}
CompanionobjectsarereallypopularintheKotlinAndroidworld.TheyaremostlyusedtodefineallelementsthatwerestaticinJava(constantfields,staticcreators,andsoon),buttheyalsoprovideadditionalcapabilities.
EnumclassesEnumeratedtype(enum)isadatatypeconsistingofasetofnamedvalues.Todefineanenumtype,weneedtoaddtheenumkeywordtotheclassdeclarationheader:
enumclassColor{
RED,
ORANGE,
BLUE,
GRAY,
VIOLET
}
valfavouriteColor=Color.BLUE
Toparsestringintoenum,usethevalueOfmethod(likeinJava):
valselectedColor=Color.valueOf("BLUE")
println(selectedColor==Color.BLUE)//prints:true
OrtheKotlinhelpermethod:
valselectedColor=enumValueOf<Color>("BLUE")
println(selectedColor==Color.BLUE)//prints:true
TodisplayallvaluesintheColorenum,usevaluesfunction(likeinJava):
for(colorinColor.values()){
println("name:${it.name},ordinal:${it.ordinal}")
}
OrtheKotlinenumerateValueshelpermethod:
for(colorinenumValues<Color>()){
println("name:${it.name},ordinal:${it.ordinal}")
}
//Prints:
name:RED,ordinal:0
name:ORANGE,ordinal:1
name:BLUE,ordinal:2
name:GRAY,ordinal:3
name:VIOLET,ordinal:4
Theenumtypecanalsohaveitsconstructorandtherecanbecustomdataassociatedtoeachenumconstant.Let'saddpropertieswithvaluesofred,green,and
bluecolorcomponents:
enumclassColor(valr:Int,valg:Int,valb:Int){
RED(255,0,0),
ORANGE(255,165,0),
BLUE(0,0,255),
GRAY(49,79,79),
VIOLET(238,130,238)
}
valcolor=Color.BLUE
valrValue=color.r
valgValue=color.g
valbValue=color.b
Havingthesevalues,wecandefineafunctionthatwillcalculateRGBvalueforeachcolor.
Noticethatthelastconstant(VIOLET)isfollowedbyasemicolon.ThisisararesituationwhereasemicolonisactuallyrequiredinKotlincode.Itseparatestheconstantdefinitionsfromthememberdefinitions:
enumclassColor(valr:Int,valg:Int,valb:Int){
BLUE(0,0,255),
ORANGE(255,165,0),
GRAY(49,79,79),
RED(255,0,0),
VIOLET(238,130,238);
funrgb()=rshl16+gshl8+b
}
funprintHex(num:Int){
println(num.toString(16))
}
printHex(Color.BLUE.rgb())//Prints:ff
printHex(Color.ORANGE.rgb())//Prints:ffa500
printHex(Color.GRAY.rgb())//Prints:314f4f
Thergb()methodaccessesther,g,andbvariabledataforaparticularenumandcalculatesthevalueforeachenumelementseparately.WecanalsoaddavalidationfortheenumconstructorargumentsusingtheinitblockandtheKotlinstandardlibraryrequirefunction:
enumclassColor(valr:Int,valg:Int,valb:Int){
BLUE(0,0,255),
ORANGE(255,165,0),
GRAY(49,79,79),
RED(255,0,0),
VIOLET(238,130,238);
init{
require(rin0..255)
require(gin0..255)
require(bin0..255)
}
funrgb()=rshl16+gshl8+b
}
Defininganincorrectenumwillresultinanexception:
GRAY(33,33,333)//IllegalArgumentException:Failedrequirement.
Therearecaseswherewewanttoassociatefundamentallydifferentbehaviorwitheachconstant.Todothiswecandefineanabstractmethodorpropertyandoverrideitineachenumblock.Let'sdefinetheenumTemperatureandtemperatureproperty:
enumclassTemperature{COLD,NEUTRAL,WARM}
enumclassColor(valr:Int,valg:Int,valb:Int){
RED(255,0,0){
overridevaltemperature=Temperature.WARM
},
ORANGE(255,165,0){
overridevaltemperature=Temperature.WARM
},
BLUE(0,0,255){
overridevaltemperature=Temperature.COLD
},
GRAY(49,79,79){
overridevaltemperature=Temperature.NEUTRAL
},
VIOLET(238,130,238{
overridevaltemperature=Temperature.COLD
};
init{
require(rin0..256)
require(gin0..256)
require(bin0..256)
}
funrgb()=(r*256+g)*256+b
abstractvaltemperature:Temperature
}
println(Color.BLUE.temperature)//prints:COLD
println(Color.ORANGE.temperature)//prints:WARM
println(Color.GRAY.temperature)//prints:NEUTRAL
Now,eachcolorcontainsnotonlyRGBinformation,butalsoanadditionalenumdescribingitstemperature.Wehaveaddedaproperty,butinananalogicalwaywecouldaddcustommethodstoeachenumelement.
InfixcallsfornamedmethodsInfixcallsareoneofKotlin'sfeaturesthatallowustocreatemorefluidandreadablecode.Itallowsustowritecodethatisclosertonaturalhumanlanguage.WehavealreadyseenusageoftheinfixmethodinChapter2,LayingaFoundation,whichallowedustoeasilycreateaninstanceofaPairclass.Hereisaquickreminder:
varpair="Everest"to8848
ThePairclassrepresentsagenericpairoftwovalues.Thereisnomeaningattachedtovaluesinthisclass,soitcanbeusedforanypurpose.Pairisadataclass,soitcontainsalldataclassmethods(equals,hashCode,component1,andsoon).HereisadefinitionofthePairclassfromtheKotlinstandardlibrary:
publicdataclassPair<outA,outB>(//1
publicvalfirst:A,
publicvalsecond:B
):Serializable{
publicoverridefuntoString():String="($first,$second)"
//2
}
1. MeaningofthisoutmodifierusedbehindagenerictypewillbedescribedinChapter6,Genericareyourfriends.
2. PairshaveacustomtoStringmethod.Thisisimplementedtomakeprintedsyntaxmorereadablewhilefirstandsecondnamesarenotmeaningfulinmostusagecontexts.
Beforewedivedeeperandlearnhowtodefineourowninfixmethod,let'stranslatejustthepresentedcodeintoamorefamiliarform.Eachinfixmethodcanbeusedlikeanyothermethod:
valmountain="Everest";
varpair=mountain.to(8848)
Initsessence,theinfixnotationissimplytheabilitytocallamethodwithoutusingthedotoperatorandcalloperator(parentheses).Theinfixnotationonlylooksdifferent,butit'sstillaregularmethodcallunderneath.Inbothforegoingexamples,wesimplycallthetomethodontheStringclassinstance.toisan
extensionfunctionanditwillbeexplainedinChapter7,ExtensionFunctionsandProperties,butwecanimagineasifitisamethodoftheStringclass,inthiscase,whichisjustreturninganinstanceofPaircontainingitselfandthepassedargument.WecanoperateonthereturnedPairlikeonanydataclassobject:
valmountain="Everest";
varpair=mountain.to(8848)
println(pair.first)//prints:Everest
println(pair.second)//prints:8848
InKotlin,thismethodisallowedtobeinfixonlywhenithasasingleparameter.Also,aninfixnotationdoesnothappenautomatically--weneedtoexplicitlymarkthemethodasinfix.Let'sdefineourPointclasswiththeinfixmethod:
dataclassPoint(valx:Int,valy:Int){
infixfunmoveRight(shift:Int)=Point(x+shift,y)
}
Usageexample:
valpointA=Point(1,4)
valpointB=pointAmoveRight2
println(pointB)//prints:Point(x=3,y=4)
NoticethatwearecreatinganewPointinstance,butwecouldalsomodifyanexistingone(ifthetypewasmutable).Thisdecisionisforthedevelopertomake,butinfixismoreoftenusedtogetherwithimmutabletypes.
Wecanuseinfixmethodscombinedwithenumstoachieveveryfluentsyntax.Let'simplementnaturalsyntaxthatwillallowustodefinecardsfromaclassicplayingcarddeck.Itincludes52:13ranksofeachofthefoursuits:clubs,diamonds,hearts,andspades.
Sourcefortheprecedingimage:https://mathematica.stackexchange.com/questions/16108/standard-deck-of-52-playing-cards-in-curated-data
Thegoalistodefinethesyntaxthatwillallowustodefineacardfromitssuitandrankitthisway:
valcard=KINGofHEARTS
Firstofall,weneedtwoenumstorepresentalltheranksandsuits:
enumclassSuit{
HEARTS,
SPADES,
CLUBS,
DIAMONDS
}
enumclassRank{
TWO,THREE,FOUR,FIVE,
SIX,SEVEN,EIGHT,NINE,
TEN,JACK,QUEEN,KING,ACE;
}
Thenweneedaclassthatwillrepresentacardcomposedofaparticularrankandparticularsuite:
dataclassCard(valrank:Rank,valsuit:Suit)
NowwecaninstantiateaCardclasslikethis:
valcard=Card(Rank.KING,Suit.HEARTS)
Tosimplifythesyntax,weintroduceanewinfixmethodintotheRankenum:
enumclassRank{
TWO,THREE,FOUR,FIVE,
SIX,SEVEN,EIGHT,NINE,
TEN,JACK,QUEEN,KING,ACE;
infixfunof(suit:Suit)=Card(this,suit)
}
ThiswillallowustocreateaCardcalllikethis:
valcard=Rank.KING.of(Suit.HEARTS)
Becausethemethodismarkedasinfix,wecanremovethedotcalloperatorandparentheses:
valcard=Rank.KINGofSuit.HEARTS
Usageofstaticimportswillallowustoshortenthesyntaxevenmoreandachieveourfinalresult:
importRank.KING
importSuit.HEARTS
valcard=KINGofHEARTS
Besidesbeingsupersimple,thiscodeisalso100%type-safe.WecanonlydefinecardsusingpredefinedenumsofRankandSuit,soweareunabletodefinesomefictionalcardbymistake.
VisibilitymodifiersKotlinsupportsfourtypesofvisibilitymodifiers(accessmodifiers)--private,protected,public,andinternal.Kotlindoesnotsupportpackage-privateJavamodifiers.ThemaindifferenceisthatthedefaultvisibilitymodifierinKotlinispublic,andit'snotrequiredtospecifyitexplicitly,soitcanbeomittedforaparticulardeclaration.Allofthemodifierscanbeappliedtovariouselementsdividedintotwomaingroupsbasedontheirdeclarationsite:top-levelelementsandnestedmembers.
AquickreminderfromChapter3,PlayingwithFunctions,toplevelelementsareelementsdeclareddirectlyinsidetheKotlinfile,asopposedtoelementsnestedinsideaclass,object,interface,orfunction.InJava,wecoulddeclareonlyclassesandinterfacesatthetoplevel,whileKotlinalsoallowsfunctions,objects,properties,andextensionsthere.
Firstwehavetop-levelelementsvisibilitymodifiers:
public(default):Elementisvisibleeverywhere.private:Elementisvisibleinsidethefilecontainingthedeclaration.protected:Notavailableattoplevel.internal:Elementisvisibleeverywhereinthesamemodule.Itispublicforelementsinthesamemodule.
WhatisamoduleinJavaandKotlin?
AmoduleisjustasetofKotlinfilescompiledtogether;forexample,IntelliJIDEAmodule,Gradleproject.Themodularstructureofapplicationsallowsforbetterdistributedresponsibilitiesandspeedsupbuildtime,becauseonlychangedmodulesarerecompiled.
Let'slookatanexample:
//top.kt
publicvalversion:String="3.5.0"//1
internalclassUnitConveter//3
privatefunprintSomething(){
println("Something")
}
funmain(args:Array<String>){
println(version)//1,Prints:"3.5.0"
UnitConveter()//2,Accessible
printSomething()//3,Prints:Something
}
//branch.kt
funmain(args:Array<String>){
println(version)//1,Accessible
UnitConveter()//2,Accessible
printSomething()//3,Error
}
//main.ktinanothermodule
funmain(args:Array<String>){
println(version)//1,Accessible
UnitConveter()//2,Error
printSomething()//3,Accessible
}
1. versionpropertyispublic,soitisaccessibleinallfiles.2. UnitConveterisaccessibleinthebranch.ktfile,whileitisinthesamemodule,
butnotinmain.ktbecauseitislocatedinanothermodule.3. TheprintSomethingfunctionisaccessibleonlyinthesamefilewhereitis
defined.
NotethatthepackageinKotlinisnotgivinganyextravisibilityprivileges.
Thesecondgroupconsistsofmembers--elements,declaredinsideatoplevelelement.Mainlythosewillbemethods,properties,constructors,sometimesobjects,companionobjects,gettersandsetters,andoccasionallynestedclassesandnestedinterfaces.Herearetheobligatoryrules:
public(default):Clientwhoseesthedeclaringclassseesitspublicmembers.private:Elementisvisibleonlyinsidetheclassorinterfacecontainingthemember.protected:Visibleinsidetheclasscontainingthedeclarationandsubclasses.Itisnotapplicableinsideanobject,becauseanobjectcannotbeopened.internal:Anyclientinsidethismodulewhoseesthedeclaringclassseesitsinternalmembers.
Let'sdefineatoplevelelement.Inthisexample,wewilldefineclass,butthe
samelogicisappliedtoanytoplevelelementthathasnestedmembers:
classPerson{
publicvalname:String="Igor"
protectedvarage:Int=23
internalfunlearn(){}
privatefunspeak(){}
}
WhenwecreateaninstanceofthePersonclass,wecanaccessonlythenamepropertymarkedwithapublicmodifierandthelearnmethodmarkedwithinternalmodifier:
//main.ktinsidethesamepackageasPersondefinition
valperson=Person()
println(person.name)//1
person.speak()//2,Error
person.age//3,Error
person.learn()//4
1. clientwhocanaccessthePersoninstance,canalsoaccessthenameproperty.2. speakmethodisaccessibleonlyinsidethePersonclass.3. agepropertyisaccessibleinsidethePersonclassanditssubclasses.4. clientinsidethemodulethatcanaccessthePersonclassinstancecanalso
accessitspublicmembers.
Inheritanceaccessibilityissimilartoexternalaccessaccessibility,butthemaindifferenceisthatthemembermarkedwiththeprotectedmodifierisalsovisibleinsidethesubclasses:
openclassPerson{
publicvalname:String="Igor"
privatefunspeak(){}
protectedvarage:Int=23
internalfunlearn(){}
}
classStudent():Person(){
fundoSth(){
println(name)
learn()
print(age)
//speak()//1
}
}
1. IntheStudentsubclasswecanaccessmembersmarkedwithpublic,protected,andinternal,butnotmembersmarkedwithprivatemodifier.
InternalmodifierandJavabytecodeItisprettyobvioushowpublic,private,andprotectedmodifiersarecompiledtoJavawhiletheyhavedirectanalogs.ButthereisaproblemwiththeinternalmodifierbecauseithasnodirectanaloginJavasothereisalsonosupportonJavabytecode.Thisiswhytheinternalmodifierisactuallycompiledtothepublicmodifier,andtocommunicatethatitshouldn'tbeusedinJava,itsnameismashed(changedsothatitisnotusableanymore).Forexample,whenwehavetheFooclass:
openclassFoo{
internalfunboo(){}
}
ItcouldbepossibletouseitfromJavathisway:
publicclassJava{
voida(){
newFoo().boo$production_sources_for_module_SmallTest();
}
}
ItisprettycontroversialthatinternalvisibilityisguardedbyKotlinanditcanbebypassedusingaJavaadapter,butthereisnootherpossibilitytoimplementit.
Besidesdefiningvisibilitymodifiersinaclass,wearealsoabletooverridethemwhileoverridingamember.Thisgivesustheabilitytoweakenaccessrestrictionsintheinheritancehierarchy:
openclassPerson{
protectedopenfunspeak(){}
}
classStudent():Person(){
publicoverridefunspeak(){
}
}
valperson=Person()
//person.speak()//1
valstudent=Student()
student.speak()//2
1. Errorspeakmethodisnotaccessiblebecauseit'sprotected.2. Visibilityofthespeakmethodwaschangedtopublicsowecanaccessit.
Definingmodifiersformembersandtheirvisibilityscopeisquitestraightforward,solet'sseehowtodefineclassandconstructorvisibility.Asweknow,primaryconstructordefinitionisintheclassheader,sotwovisibilitymodifiersarerequiredinasingleline:
internalclassFruitprivateconstructor{
varweight:Double?=null
companionobject{
funcreate()=Fruit()
}
}
Assumingtheprecedingclassisdefinedatthetoplevel,itwillbevisibleinsidethemodule,butitcanbeonlyinstantiatedfromwithinthefilecontainingtheclassdeclaration:
varfruit:Fruit?=null//Accessible
fruit=Fruit()//Error
fruit=Fruit.create()//Accessible
Getterandsettersbydefaulthavethesamevisibilitymodifierastheproperty,butwecanmodifyit.Kotlinallowsustoplaceavisibilitymodifierbeforetheget/setkeyword:
classCar{
init{
count++;
println("Carcreated")
}
companionobject{
init{
println("Carcompanionobjectcreated")
}
varcount:Int=0
privateset
}
}
Intheprecedingexample,wehavechangedthegettervisibility.Noticethatthisapproachallowsustochangethevisibilitymodifierwithoutchangingitsdefaultimplementation(generatedbythecompiler).Now,ourinstancecounterissafe,becauseit'sread-onlyexternalclients,butwecanstillmodifythepropertyvaluefrominsidetheCarclass.
SealedclassesAsealedclassisaclasswithlimitednumberofsubclasses(sealedsubtypinghierarchy).PriortoKotlin1.1,thosesubclasseshadtobedefinedinsideasealedclassbody.Kotlin1.1weakenedthisrestrictionandallowedustodefinesealedclasssubclassesinthesamefileasasealedclassdeclaration.Alltheclassesaredeclaredclosetoeachother,sowecaneasilyseeallpossiblesubclassesbysimplylookingatonefile:
//vehicle.kt
sealedclassVehicle()
classCar:Vehicle()
classTruck:Vehicle()
classBus:Vehicle()
Tomarkaclassassealed,simplyaddasealedmodifiertotheclassdeclarationheader.TheprecedingdeclarationmeansthattheVehicleclasscanbeonlyextendedbythreeclassesCar,Truck,andBusbecausetheyaredeclaredinsidethesamefile.Wecouldaddafourthclassinourvehicle.ktfile,butitwouldnotbepossibletodefinesuchaclassinanotherfile.
ThesealedsubtypingrestrictionappliesonlytodirectinheritorsoftheVehicleclass.ThismeansthatVehiclecanbeextendedonlybyclassesdefinedinthesamefile(Car,Truck,orBus),butassumingthatCar,Truck,orBusclasseswouldbeopenthentheycanbeextendedbyaclassdeclaredinsideanyfile:
//vehicle.kt
sealedclassVehicle()
openclassBus:Vehicle()
//data.kt
classSchoolBus:Bus()
Topreventthisbehavior,wewouldneedtoalsomarkCar,Truck,orBusclassesassealed:
//vehicle.kt
sealedclassVehicle()
sealedclassBus:Vehicle()
//data.kt
classSchoolBus:Bus()//ErrorcannotaccessBus
Thesealedclassesworkreallywellwiththewhenexpression.Thereisnoneedforanelseclause,becauseacompilercanverifythateachsubclassofasealedclasshasacorrespondingclauseinsidethewhenblock:
when(vehicle){
isCar->println("Cantransport4people")
isTruck->println("Cantransportfurnitures")
isBus->println("Cantransport50people")
}
WecansafelyaddanewsubclasstotheVehicleclass,becauseifsomewhereintheapplicationthecorrespondingclauseofthewhenexpressionismissing,theapplicationwillnotcompile.ThisfixesproblemswiththeJavaswitchstatement,whereprogrammersoftenforgettoaddpropercapsules,whichleadstoprogramcrashesatruntimeorundetectedbugs.
Sealedclassesareabstractbydefault,soaabstractmodifierisredundant.Sealedclassescanneverbeopenorfinal.Wecanalsosubstituteasubclasswithobjectsincaseweneedtomakesurethatonlyasingleinstanceexists:
sealedclassEmployee()
classProgrammer:Employee()
classManager:Employee()
objectCEO:Employee()
Theprecedingdeclarationnotonlyprotectstheinheritancehierarchy,butalsolimitsCEOtoasingleinstance.Thereareafewinterestingapplicationsforsealedclassesthatexceedthescopeofthisbook,butit'sgoodtobeawareofthem:
Definedatatypessuchasalinkedlistorbinarytree(https://en.wikipedia.org/wiki/Algebraic_data_type).ProtectinheritancehierarchywhenbuildinganapplicationmoduleorlibrarybydisallowingclientstoextendourclassandstillkeeptheabilitytoextenditbyourselvesStatemachinewheresomestatescontaindatathatmakesnosenseinotherstates(https://en.wikipedia.org/wiki/Finite-state_machine)Listofpossibletokenstypesforlexicalanalysis
NestedclassesAnestedclassisaclassdefinedinsideanotherclass.Nestingsmallclasseswithintop-levelclassesplacesthecodeclosertowhereitisused,andallowsabetterwayofgroupingclasses.TypicalexamplesareTree/Leaflistenersorpresenterstates.KotlinsimilartoJavaallowsustodefineanestedclassandtherearetwomainwaystodoso.Wecandefineclassasamemberinsideaclass:
classOuter{
privatevalbar:Int=1
classNested{
funfoo()=2
}
}
valdemo=Outer.Nested().foo()//==2
TheprecedingexampleallowsustocreateaninstanceofaNestedclasswithoutcreatinginstancesofanOuterclass.Inthiscase,aclasscannotreferdirectlytoinstancevariablesormethodsdefinedinitsenclosingclass(itcanusethemonlythroughanobjectreference).ThisisequivalentofaJavastaticnestedclassandingeneralstaticmembers.
Tobeabletoaccessmembersofanouterclass,wemustcreateasecondkindofclassbymarkinganestedclassasinner:
classOuter{
privatevalbar:Int=1
innerclassInner{
funfoo()=bar
}
}
valouter=Outer()
valdemo=outer.Inner().foo()//==1
NowtoinstantiatetheinnerclasswemustfirstinstantiatetheOuterclass.Inthiscase,theInnerclasscanaccessallthemethodsandpropertiesdefinedintheouterclassandsharestatewithouterclass.OnlyasingleinstanceofInnerclasscanexistperinstanceoftheOuterclass.Let'ssumupthedifferences:
Behavior Class(member)Innerclass(member)
BehaveasaJavastaticmember Yes No
Instanceofthisclasscanexistwithoutaninstanceofenclosingclass Yes No
HasReferencetoouterclass No Yes
Sharestatewithouterclass(canaccessouterclassmembers) No Yes
Numberofinstances Unlimited Oneperouterclassinstance
Whendecidingwhetherweshoulddefineinnerclassortop-levelclassweshouldthinkaboutpotentialclassusage.Iftheclassisonlyusefulforasingleclassinstanceweshoulddeclareitasinner.Ifaninnerclassatsomepointwouldbeusefulinanothercontextthanservingitsouterclass,thenweshoulddeclareitasatop-levelclass.
ImportaliasesAnaliasisawaytointroducenewnamesfortypes.Ifthetypenameisalreadyusedinthefile,isinappropriate,ortoolong,youcanintroduceadifferentnameanduseitinsteadoftheoriginaltypename.Aliasdoesnotintroduceanewtypeanditisavailableonlybeforecompiletime(whenwritingcode).Thecompilerreplacesaclassaliaswithanactualclass,soitdoesnotexistatruntime.
Sometimesweneedtouseafewclasseswiththesamenameinasinglefile.Forexample,InterstitialAdtypeisdefinedbothintheFacebookandGoogleadvertisinglibraries.Let'ssupposethatwewanttousethembothinasinglefile.Thissituationiscommoninprojectswhereweneedbothadprovidersimplementedtoallowforaprofitcomparisonbetweenthem.Theproblemisthatusingbothdatatypesinasinglefilewouldmeanthatweneedtoaccessoneorbothofthembyafullyqualifiedclassname(namespace+classname):
importcom.facebook.ads.InterstitialAd
valfbAd=InterstitialAd(context,"...")
valgoogleAd=com.google.android.gms.ads.InterstitialAd(context)
Qualifiedversusunqualifiedclassname
Theunqualifiedclassnameissimplythenameoftheclass;forexample,Box.Aqualifiedclassnameisanamespacecombinedwithaclassname;forexample,com.test.Box.
Inthesesituations,peopleoftensaythatthebestfixistorenameoneoftheclasses,butsometimesthismaynotbepossible(theclassisdefinedinanexternallibrary)ordesirable(classnameisconsistentwithbackenddatabasetable).Inthissituation,whereboththeclassesarelocatedinanexternallibrary,thesolutionforclassnamingconflictistouseanimportalias.WecanuseittorenameGoogleInterstitialAdtoGoogleAd,andFacebookInterstitialAdtoFbAd:
importcom.facebook.ads.InterstitialAdasFbAd
importcom.google.android.gms.ads.InterstitialAdasGoogleAd
Andnowwecanusethesealiasesaroundthefileasiftheywereactualtypes:
valfbAd=FbAd(context,"...")
valgoogleAd=GoogleAd(context)
Usingtheimportalias,wecanexplicitlyredefinenamesofaclassthatareimportedintoafile.Inthissituation,wedidn'thavetousetwoaliases,butthisservestoimprovereadability--it'sbettertohaveFbAdandGoogleAdthanInterstitialAdandGoogleAd.Wedon'thavetousefullyqualifiedclassnamesanymore,becausewesimplysaidtothecompiler"eachtimewhenyouencounterGoogleAdaliastranslateittocom.google.android.gms.ads.InterstitialAdduringcompilationandeachtimewhenyouencounterFbAdaliastranslateittocom.facebook.ads.InterstitialAd.Importaliasworksonlyinsideafilewherealiasisdefined.
SummaryInthischapter,wehavediscussedconstructs,whicharebuildingsblocksforobject-orientedprogramming.We'velearnedhowtodefineinterfacesandvariousclassesandthedifferencebetweeninner,sealed,enum,anddataclasses.Welearnedthatallelementsarepublicbydefaultandallclasses/interfacesarefinal(bydefault),soweneedtoexplicitlyopenthemtoallowinheritanceandmembersoverriding.
Wediscussedhowtodefineproperdatamodelsusingveryconcisedataclassescombinedwithevenmorepowerfulproperties.Weknowhowtoproperlyoperateondatausingvariousmethodsgeneratedbythecompilerandhowtooverloadoperators.
Welearnedhowtocreatesingletonsbyusingobjectdeclarationsandhowtodefineobjectsofananonymoustypethatmayextendsomeclassand/orimplementsomeinterfaceusingobjectexpressions.Wealsopresentedusageofthelateinitmodifierthatallowsustodefinenon-nullabledatatypeswithinitializationdelayedintime.
Inthenextchapter,wewillcoverthemorefunctionalsideofKotlinbylookingintoconceptsrelatedtofunctionalprogramming(FP).Wewilldiscussfunctionaltypes,lambdas,andhigher-orderfunctions.
FunctionsasFirst-ClassCitizensInthepreviouschapter,wesawhowKotlinfeaturesrelatetoOOP.ThischapterwillintroduceadvancedfunctionalprogrammingfeaturesthatwerepreviouslynotpresentinstandardAndroiddevelopment.SomeofthemwereintroducedinJava8(inAndroidthroughtheRetrolambdaplugin),butKotlinintroducesmanymorefunctionalprogrammingfeatures.
Thischapterisabouthigh-levelfunctionsandfunctionsasfirst-classcitizens.Mostoftheconceptsaregoingtobefamiliartoreaderswhohaveusedfunctionallanguagesinthepast.
Inthischapter,wewillcoverthefollowingtopics:
FunctiontypesAnonymousfunctionsLambdaexpressionsImplicitnameofasingleparameterinalambdaexpressionHigher-orderfunctionsLastlambdainargumentconventionJavaSingleAbstractMethod(SAM)lambdainterfaceJavamethodswithJavaSingleAbstractMethodonparametersusageNamedparametersinfunctiontypesTypealiasesInlinefunctionsFunctionreferences
FunctiontypeKotlinsupportsfunctionalprogramming,andfunctionsarefirst-classcitizensinKotlin.Afirst-classcitizen,inagivenprogramminglanguage,isatermthatdescribesanentitythatsupportsalltheoperationsgenerallyavailabletootherentities.Theseoperationstypicallyincludebeingpassedasanargument,returnedfromafunction,andassignedtoavariable.Thesentence"afunctionisafirst-classcitizeninKotlin"shouldthenbeunderstoodas:itispossibleinKotlintopassfunctionsasanargument,returnthemfromfunctions,andassignthemtovariables.WhileKotlinisastaticallytypedlanguage,thereneedstobeafunctiontypedefinedtoallowtheseoperations.InKotlin,thenotationusedtodefineafunctiontypeisfollowing:
(typesofparameters)->returntype
Herearesomeexamples:
(Int)->Int:AfunctionthattakesIntasanargumentandreturnsInt()->Int:AfunctionthattakesnoargumentsandreturnsInt(Int)->Unit:AfunctionthattakesIntanddoesnotreturnanything(onlyUnit,whichdoesnotneedtobereturned)
Herearesomeexamplesofpropertiesthatcanholdfunctions:
lateinitvara:(Int)->Int
lateinitvarb:()->Int
lateinitvarc:(String)->Unit
Thetermfunctiontypeismostoftendefinedasthetypeofavariableorparametertowhichafunctioncanbeassigned,ortheargumentorresulttypeofahigher-orderfunctiontakingorreturningafunction.InKotlin,thefunctiontypecanbetreatedlikeaninterface.
WewillseelaterinthischapterthatKotlinfunctionscantakeotherfunctionsinarguments,orevenreturnthem:
funaddCache(function:(Int)->Int):(Int)->Int{
//code
}
valfibonacciNumber:(Int)->Int=//functionimplementation
valfibonacciNumberWithCache=addCache(fibonacciNumber)
Ifafunctioncantakeorreturnafunction,thenthefunctiontypealsoneedstobeabletodefinefunctionsthattakeafunctionasanargument,orreturnafunction.Thisisdonebysimplyplacingafunctiontypenotationasaparameterorareturntype.Herearesomeexamples:
(String)->(Int)->Int:AfunctionthattakesStringandreturnsafunctionthattakesInttypeandreturnsInt.(()->Int)->String:Afunctionthattakesanotherfunctionasanargument,andreturnsStringtype.FunctioninargumenttakesnoargumentsandreturnsInt.
Eachpropertywithafunctiontypecanbecalledlikeafunction:
vali=a(10)
valj=b()
c("SomeString")
Functionscannotonlybestoredinvariables,theycanalsobeusedasageneric.Forexample,wecankeepthefunctionsinthelist:
vartodoList:List<()->Unit>=//...
for(taskintodoList)task()
Theprecedinglistcanstorefunctionswiththe()->Unitsignature.
Whatisfunctiontypeunderthehood?Underthehood,functiontypesarejustasyntacticsugarforgenericinterfaces.Let'slookatsomeexamples:
The()->UnitsignatureisaninterfaceforFunction0<Unit>.TheexpressionisFunction0,becauseithaszeroparameters,andUnitbecauseitisthereturntype.The(Int)->UnitsignatureisinterfaceforFunction1<Int,Unit>.TheexpressionisFunction1becauseithasoneparameter.The()->(Int,Int)->StringsignatureisaninterfaceforFunction0<Function2<Int,Int,String>>.
Alloftheseinterfaceshaveonlyonemethod,invoke,whichisanoperator.Itallowsanobjecttobeusedlikeafunction:
vala:(Int)->Unit=//...
a(10)//1
a.invoke(10)//1
1. Thesetwostatementshavethesamemeaning
Functioninterfacesarenotpresentinastandardlibrary.Theyaresyntheticcompiler-generatedtypes(theyaregeneratedduringcompilation).Becauseofthis,thereisnoartificiallimitinnumberoffunctiontypearguments,andthestandardlibrarysizeisnotincreased.
AnonymousfunctionsOnewayofdefiningafunctionasanobjectisbyusinganonymousfunctions.Theyworkthesamewayasnormalfunctions,buttheyhavenonamebetweenthefunkeywordandtheparametersdeclaration,sobydefaulttheyaretreatedasobjects.Hereareafewexamples:
vala:(Int)->Int=fun(i:Int)=i*2//1
valb:()->Int=fun():Int{return4}
valc:(String)->Unit=fun(s:String){println(s)}
1. Thisisananonymoussingleexpressionfunction.Notethatlikeinanormalsingleexpressionfunction,thereturntypedoesnotneedtobespecifiedwhenitisinferredfromtheexpressionreturntype.
Considerthefollowingusage:
//Usage
println(a(10))//Prints:20
println(b())//Prints:4
c("Kotlinrules")//Prints:Kotlinrules
Inthepreviousexamples,functiontypesweredefinedexplicitly,butwhileKotlinhasagoodtypeinferencesystem,thefunctiontypecanalsobeinferredfromtypesdefinedbyananonymousdefaultfunction:
vara=fun(i:Int)=i*2
varb=fun():Int{return4}
varc=fun(s:String){println(s)}
Italsoworksintheoppositeway.Whenwedefinethetypeofaproperty,thenwedon'tneedtosetparametertypesinanonymousfunctionsexplicitly,becausetheyareinferred:
vara:(Int)->Int=fun(i)=i*2
varc:(String)->Unit=fun(s){println(s)}
Ifwecheckoutthemethodsoffunctiontypes,thenwewillseethatthereisonlytheinvokemethodinside.Theinvokemethodisanoperatorfunction,anditcanbeusedinthesamewayasfunctioninvocation.Thisiswhythesameresultcanbeachievedbyusingtheinvokecallinsidebrackets:
println(a.invoke(4))//Prints:8
println(b.invoke())//Prints:4
c.invoke("Hello,World!")//Prints:Hello,World!
Thisknowledgecanbehelpful,forexample,whenwearekeepingfunctioninanullablevariable.Wecan,forexample,usetheinvokemethodbyusingthesafecall:
vara:((Int)->Int)?=null//1
if(false)a=fun(i:Int)=i*2
print(a?.invoke(4))//Prints:null
1. Variableaisnullable,weareusinginvokebyasafecall.
Let'slookatanAndroidexample.Weoftenwanttodefineasingleerrorhandlerthatwillincludemultipleloggingmethodsandpassittodifferentobjectsasanargument.Hereishowwecanimplementitusinganonymousfunctions:
valTAG="MainActivity"
valerrorHandler=fun(error:Throwable){
if(BuildConfig.DEBUG){
Log.e(TAG,error.message,error)
}
toast(error.message)
//Othermethods,like:Crashlytics.logException(error)
}
//Usageinproject
valadController=AdController(errorHandler)
valpresenter=MainPresenter(errorHandler)
//Usage
valerror=Error("ExampleError")
errorHandler(error)//Logs:MainActivity:ExampleError
Anonymousfunctionsaresimpleanduseful.Theyareasimplewayofdefiningfunctionsthatcanbeusedandpassedasobjects.Butthereisasimplerwayofachievingsimilarbehavior,anditiscalledlambdaexpressions.
LambdaexpressionsThesimplestwaytodefineanonymousfunctionsinKotlinisbyusingafeaturecalledlambdaexpressions.TheyaresimilartoJava8lambdaexpressions,butthebiggestdifferenceisthatKotlinlambdasareactuallyclosures,sotheyallowustochangevariablesfromthecreationcontext.ThisisnotallowedinJava8lambdas.Wewilldiscussthisdifferencelaterinthissection.Let'sstartwithsomesimpleexamples.LambdaexpressionsinKotlinhavethefollowingnotation:
{arguments->functionbody}
Insteadofreturn,resultofthelastexpressionisreturned.Herearesomesimplelambdaexpressionexamples:
{1}:Alambdaexpressionthattakesnoargumentsandreturns1.Itstypeis()->Int.{s:String->println(s)}:AlambdaexpressionthattakesoneargumentoftypeString,andprintsit.ItreturnsUnit.Itstypeis(String)->Unit.{a:Int,b:Int->a+b}:AlambdaexpressionthattakestwoIntargumentsandreturnsthesumofthem.Itstypeis(Int,Int)->Int.
Functionswedefinedinthepreviouschaptercanbedefinedusinglambdaexpressions:
vara:(Int)->Int={i:Int->i*2}
varb:()->Int={4}
varc:(String)->Unit={s:String->println(s)}
Whilethereturnedvalueistakenfromthelaststatementinlambdaexpressions,returnisnotallowedunlessithasareturnstatementqualifiedbyalabel:
vara:(Int)->Int={i:Int->returni*2}
//Error:Returnisnotallowedthere
varl:(Int)->Int=l@{i:Int->return@li*2}
Lambdaexpressionscanbemultiline:
valprintAndReturn={i:Int,j:Int->
println("Icalculate$i+$j")
i+j//1
}
1. Thisisthelaststatement,sotheresultofthisexpressionwillbeareturnedvalue.
Multiplestatementscanalsobedefinedinasinglelinewhentheyareseparatedbysemicolons:
valprintAndReturn={i:Int,j:Int->println("Icalculate$i+$j");
i+j}
Alambdaexpressiondoesnotneedtoonlyoperateonvaluesprovidedbyarguments.LambdaexpressionsinKotlincanuseallpropertiesandfunctionsfromthecontextwheretheyarecreated:
valtext="Text"
vara:()->Unit={println(text)}
a()//Prints:Text
a()//Prints:Text
ThisisthebiggestdifferencebetweenKotlinandJava8lambdausage.BothJavaanonymousobjectsandJava8lambdaexpressionsallowustousefieldsfromthecontext,butJavadoesnotallowustoassigndifferentvaluestothesevariables(Javavariablesusedinlambdamustbefinal):
Kotlinhasgoneastepaheadbyallowinglambdaexpressionsandanonymousfunctionstomodifythesevariables.Lambdaexpressionsthatencloselocalvariablesandallowustochangetheminsidethefunctionbodyarecalledclosures.Kotlinfullysupportsclosuredefinition.Toavoidconfusionbetweenlambdasandclosures,inthisbook,wewillalwayscallbothofthemlambdas.Let'slookatanexample:
vari=1
vala:()->Int={++i}
println(i)//Prints:1
println(a())//Prints:2
println(i)//Prints:2
println(a())//Prints:3
println(i)//Prints:3
Lambdaexpressionscanuseandmodifyvariablesfromthelocalcontext.Hereisanexampleofcounter,wherethevalueiskeptinalocalvariable:
funsetUpCounter(){
varvalue:Int=0
valshowValue={counterView.text="$value"}
counterIncView.setOnClickListener{value++;showValue()}
//1
counterDecView.setOnClickListener{value--;showValue()}
//1
}
1. HereishowViewonClickListenercanbesetinKotlinusingalambdaexpression.ThiswillbedescribedintheJavaSAMsupportinKotlinsection.
Thankstothisfeature,itissimplertouselambdaexpressions.Notethat,intheprecedingexample,theshowValuetypewasnotspecified.ThisisbecauseinKotlinlambdas,typingargumentsisoptionalwhenthecompilercaninferitfromthecontext:
vala:(Int)->Int={i->i*2}//1
valc:(String)->Unit={s->println(s)}//2
1. TheinferredtypeofiisInt,becausethefunctiontypedefinesanIntparameter.
2. TheinferredtypeofsisString,becausethefunctiontypedefinesaStringparameter.
Aswecanseeinthefollowingexample,wedon'tneedtospecifythetypeofparameterbecauseitisinferredfromthetypeoftheproperty.Typeinferencealsoworksintheanotherway--wecandefinethetypeofalambdaexpression'sparametertoinferthepropertytype:
valb={4}//1
valc={s:String->println(s)}//2
vala={i:Int->i*2}//3
1. Theinferredtypeis()->Int,because4isIntandthereisnoparametertype.2. Theinferredtypeis(String)->Unit,becausetheparameteristypedasString,
andthereturntypeoftheprintlnmethodisUnit.
3. Theinferredtypeis(Int)->Int,becauseiistypedasInt,andthereturntypeofthetimesoperationfromIntisalsoInt.
Thisinferencesimplifieslambdaexpressiondefinition.Often,whenwearedefininglambdaexpressionsasfunctionparameters,wedon'tneedtospecifyparametertypeseachtime.Butthereisalsoanotherbenefit--whiletheparametertypecanbeinferred,asimplernotationforsingleparameterlambdaexpressionscanbeused.Let'sdiscussthisinthenextsection.
ImplicitnameofasingleparameterWecanomitlambdaparameterdefinitionsandaccessparametersusingtheitkeywordwhentwoconditionsaremet:
ThereisonlyoneparameterParametertypecanbeinferredfromthecontext
Asanexample,let'sdefinethepropertiesaandcagain,butthistimeusingtheimplicitnameofasingleparameter:
vala:(Int)->Int={it*2}//1
valc:(String)->Unit={println(it)}//2
1. Sameas{i->i*2}.2. Sameas{s->println(s)}.
ThisnotationisreallypopularinKotlin,mostlybecauseitisshorteranditallowsustoavoidparametertypespecification.ItalsoimprovesthereadabilityofprocessingdefinedinLINQstyle.Thisstyleneedscomponentsthathavenotyetbeenintroduced,butjusttoshowtheidea,let'sseeanexample:
strings.filter{it.length=5}.map{it.toUpperCase()}
SupposingthatstringsisList<String>,thisexpressionfiltersstringswithalengthequalto5andconvertsthemtouppercase.
Notethatinthebodyoflambdaexpressions,wecanusemethodsoftheStringclass.Thisisbecausefunctiontype(suchas(String)->Booleanforthefilter)isinterredfromthemethoddefinition,whichinfersStringfromtheiterabletype(List<String>).Also,thetypeofthereturnedlist(List<String>)dependsonwhatisreturnedbythelambda(String).
LINQstyleispopularinfunctionallanguagesbecauseitmakesthesyntaxofcollectionsorStringprocessingreallysimpleandconcise.ItwillbediscussedinmuchmoredetailinChapter7,ExtensionFunctionsandProperties.
Higher-orderfunctionsAhigher-orderfunctionisafunctionthattakesatleastonefunctionasanargument,orreturnsafunctionasitsresult.ItisfullysupportedinKotlin,asfunctionsarefirst-classcitizens.Let'sseeitinanexample.Let'ssupposethatweneedtwofunctions:afunctionthatwilladdallBigDecimalnumbersfromlist,andafunctionthatwillgettheproduct(theresultofmultiplicationbetweenalltheelementsinthislist)ofallthesenumbers:
funsum(numbers:List<BigDecimal>):BigDecimal{
varsum=BigDecimal.ZERO
for(numinnumbers){
sum+=num
}
returnsum
}
funprod(numbers:List<BigDecimal>):BigDecimal{
varprod=BigDecimal.ONE
for(numinnumbers){
prod*=num
}
returnprod
}
//Usage
valnumbers=listOf(
BigDecimal.TEN,
BigDecimal.ONE,
BigDecimal.valueOf(2)
)
print(numbers)//[10,1,2]
println(prod(numbers))//20
println(sum(numbers))//13
Thesearereadablefunctions,butalsothesefunctionsarenearlythesame.Theonlydifferenceisname,accumulator(BigDecimal.ZEROorBigDecimal.ONE),andoperation.IfweusetheDRY(Don'tRepeatYourself)rulethenweshouldn'tleavetwopartsofsimilarcodeintheproject.Whileitiseasytodefineafunctionthatwillhavesimilarbehaviorandjustdifferintheobjectsused,itishardertodefineafunctionthatwilldifferintheoperationperformed(here,functionsdifferbytheoperationusedtoaccumulate).Solutioncomeswiththefunctiontype,becausewecanpasstheoperationasanargument.Inthisexample,itispossibletoextractthecommonmethodthisway:
funsum(numbers:List<BigDecimal>)=
fold(numbers,BigDecimal.ZERO){acc,num->acc+num}
funprod(numbers:List<BigDecimal>)=
fold(numbers,BigDecimal.ONE){acc,num->acc*num}
privatefunfold(
numbers:List<BigDecimal>,
start:BigDecimal,
accumulator:(BigDecimal,BigDecimal)->BigDecimal
):BigDecimal{
varacc=start
for(numinnumbers){
acc=accumulator(acc,num)
}
returnacc
}
//Usage
funBD(i:Long)=BigDecimal.valueOf(i)
valnumbers=listOf(BD(1),BD(2),BD(3),BD(4))
println(sum(numbers))//Prints:10
println(prod(numbers))//Prints:24
Thefoldfunctioniteratesthroughnumbersandupdatesaccusingeachelement.Notethatthefunctionparameterisdefinedlikeanyothertype,anditcanbeusedlikeanyotherfunction.Forexample,wecanhavethevarargfunctiontypeparameter:
funlongOperation(varargobservers:()->Unit){
//...
for(oinobservers)o()
}
InlongOperation,forisusedtoiterateoveralltheobserversandinvokesthemoneafteranother.Thisfunctionallowsmultiplefunctionstobeprovidedasarguments.Here'sanexample:
longOperation({notifyMainView()},{notifyFooterView()})
FunctionsinKotlincanalsoreturnfunctions.Forexample,wecandefineafunctionthatwillcreatecustomerrorhandlerswiththesameerrorloggingbutdifferenttags:
funmakeErrorHandler(tag:String)=fun(error:Throwable){
if(BuildConfig.DEBUG)Log.e(tag,error.message,error)
toast(error.message)
//Othermethods,like:Crashlytics.logException(error)
}
//Usageinproject
valadController=AdController(makeErrorHandler("AdinMainActivity"))
valpresenter=MainPresenter(makeErrorHandler("MainPresenter"))
//Usage
valexampleHandler=makeErrorHandler("ExampleHandler")
exampleHandler(Error("SomeError"))//Logs:ExampleHandler:SomeError
Thethreemostcommoncaseswhenfunctionsinargumentsareusedare:
ProvidingoperationstofunctionsTheobserver(listener)patternCallbackafterathreadedoperation
Let'slookatthemindetail.
ProvidingoperationstofunctionsAswesawintheprevioussection,sometimeswewanttoextractcommonfunctionalityfromfunctions,buttheydifferinanoperationtheyuse.Insuchsituations,wecanstillextractthisfunctionality,butweneedtoprovideanargumentwithoperationthatdistinguishesthem.Thisway,anycommonpatterncanbeextractedandreused.Forexample,weoftenonlyneedelementsofthelistthatmatchsomepredicate,suchaswhenweonlywanttoshowelementsthatareactive.Classically,thiswouldbeimplementedlikethis:
varvisibleTasks=emptyList<Task>()
for(taskintasks){
if(task.active)
visibleTasks+=task
}
Whileitisacommonoperation,wecanextractthefunctionalityofonlyfilteringsomeelementsaccordingtothepredicatetoseparatethefunctionanduseitmoreeasily:
fun<T>filter(list:List<T>,predicate:(T)->Boolean){
varvisibleTasks=emptyList<T>()
for(eleminlist){
if(predicate(elem))
visibleTasks+=elem
}
}
varvisibleTasks=filter(tasks,{it.active})
Thiswayofusinghigher-orderfunctionsisveryimportantanditwillbedescribedoftenthroughoutthebook,butthisisnottheonlywaythathigher-orderfunctionsareoftenused.
Observer(Listener)patternWeusetheObserver(Listener)patternwhenwewanttoperformoperationswhenaneventoccurs.InAndroiddevelopment,observersareoftensettoviewelements.Commonexamplesareon-clicklisteners,on-touchlisteners,ortextwatchers.InKotlin,wecansetlistenerswithnoboilerplate.Forexample,settinglisteneronbuttonclicklooksasfollows:
button.setOnClickListener({someOperation()})
NotethatthesetOnClickListenerisaJavamethodfromtheAndroidlibrary.Later,wewillseeindetailwhywecanuseitwithlambdaexpression.Thecreationoflistenersisverysimple.Hereisanexample:
varlisteners:List<()->Unit>=emptyList()//1
funaddListener(listener:()->Unit){
listeners+=listener//2
}
funinvokeListeners(){
for(listenerinlisteners)listener()//3
}
1. Here,wecreateanemptylisttoholdalllisteners.2. Wecansimplyaddalistenertothelistenerslist.3. Wecaniteratethroughthelistenersandinvokethemoneafteranother.
Itishardtoimagineasimplerimplementationofthispattern.Thereisanothercommonusecasewhereparameterswithfunctiontypesarecommonlyused--callbackafterathreadedoperation.
CallbackafterathreadedoperationIfweneedtodoalongoperation,andwedon'twanttomaketheuserwaitforit,thenwehavetostartitinanotherthread.Tobeabletousecallbackafterlongoperationcalledinseparatethread,weneedtopassitasanargument.Here'sanexamplefunction:
funlongOperationAsync(longOperation:()->Unit,callback:()->Unit){
Thread({//1
longOperation()//2
callback()//3
}).start()//4
}
//Usage
longOperationAsync(
longOperation={Thread.sleep(1000L)},
callback={print("Aftersecond")}
//5,Prints:Aftersecond
)
println("Now")//6,Prints:Now
1. Here,wecreateThread.Wealsopassalambdaexpressionthatwewouldliketoexecuteontheconstructorargument.
2. Here,weareexecutingalongoperation.3. Here,westartthecallbackoperationprovidedintheargument.4. startisamethodthatstartsthedefinedthread.5. Isprintedafteroneseconddelay.6. Isprintedimmediately.
Actually,therearesomepopularalternativestousingcallbacks,suchasRxJava.Still,classiccallbacksareincommonuse,andinKotlintheycanbeimplementedwithnoboilerplate.
Thesearethemostcommonusecaseswherehigher-orderfunctionsareused.Allofthemallowustoextractcommonbehavioranddecreaseboilerplate.Kotlinallowsafewmoreimprovementsregardinghigher-orderfunctions.
CombinationofnamedargumentsandlambdaexpressionsUsingdefaultnamedargumentsandlambdaexpressionscanbereallyusefulinAndroid.Let'slookatsomeotherpracticalAndroidexamples.Let'ssupposewehaveafunctionthatdownloadselementsandshowsthemtouser.Wewilladdafewparameters:
onStart:ThiswillbecalledbeforethenetworkoperationonFinish:Thiswillbecalledafterthenetworkoperation
fungetAndFillList(onStart:()->Unit={},
onFinish:()->Unit={}){
//code
}
Then,wecanshowandhideloadingspinnerinonStartandonFinish:
getAndFillList(
onStart={view.loadingProgress=true},
onFinish={view.loadingProgress=false}
)
IfwestartitfromswipeRefresh,thenwejustneedtohideitwhenitfinishes:
getAndFillList(onFinish={view.swipeRefresh.isRefreshing=
false})
Ifwewanttomakeaquietrefresh,thenwejustcallthis:
getAndFillList()
Namedargumentsyntaxandlambdaexpressionsareaperfectmatchformulti-purposefunctions.Thisconnectsboththeabilitytochoosetheargumentswewanttoimplementandtheoperationsthatshouldbeimplemented.Ifafunctioncontainsmorethanonefunctiontypeparameter,theninmostcases,itshouldbeusedbynamedargumentsyntax.Thisisbecauselambdaexpressionsarerarelyself-explanatorywhenmorethanoneisusedasarguments.
LastlambdainargumentconventionInKotlin,higher-orderfunctionsarereallyimportant,andsoitisalsoimportanttomaketheirusageascomfortableaspossible.ThisiswhyKotlinintroducedaspecialconventionthatmakeshigher-orderfunctionsmoresimpleandclear.Itworksthisway:ifthelastparameterisafunction,thenwecandefinealambdaexpressionoutsideofthebrackets.Let'sseehowitlooksifweuseitwiththelongOperationAsyncfunction,whichisdefinedasfollows:
funlongOperationAsync(a:Int,callback:()->Unit){
//...
}
Thefunctiontypeisinthelastpositioninthearguments.Thisiswhywecanexecuteitthisway:
longOperationAsync(10){
hideProgress()
}
Thankstothelastlambdainargumentconvention,wecanlocatethelambdaafterthebrackets.Itlooksasifitisoutsidethearguments.
Asanexample,let'sseehowtheinvocationofcodeinanotherthreadcanbedoneinKotlin.ThestandardwayofstartinganewthreadinKotlinisbyusingthethreadfunctionfromKotlinstandardlibrary.Itsdefinitionisasfollows:
publicfunthread(
start:Boolean=true,
isDaemon:Boolean=false,
contextClassLoader:ClassLoader?=null,
name:String?=null,
priority:Int=-1,
block:()->Unit):Thread{
//implementation
}
Aswecansee,theblockparameter,whichtakesoperationsthatshouldbeinvokedasynchronously,isinthelastposition.Allotherparametershaveadefaultargumentdefined.Thatiswhywecanusethethreadfunctioninthisway:
thread{/*code*/}
Thethreaddefinitionhaslotsofotherarguments,andwecansetthemeitherbyusingnamedargumentsyntaxorjustbyprovidingthemoneafteranother:
thread(name="SomeThread"){/*...*/}
thread(false,false){/*...*/}
Thelastlambdainargumentconventionissyntacticsugar,butitmakesitmucheasiertousehigher-orderfunctions.Thesearethetwomostcommoncaseswherethisconventionreallymakesadifference:
NamedcodesurroundingProcessingdatastructuresusingLINQ-style
Let'slookatthemclosely.
NamedcodesurroundingSometimesweneedtomarksomepartofthecodetobeexecutedindifferentway.Thethreadfunctionisthiskindofsituation.Weneedsomecodetobeexecutedasynchronously,sowesurrounditwithbracketstartingfromthethreadfunction:
thread{
operation1()
operation2()
}
Fromtheoutside,itlooksasifitisapartofcodethatissurroundedbyablocknamedthread.Let'slookatanotherexample.Let'ssupposethatwewanttologtheexecutiontimeofacertaincodeblock.Asahelper,wewilldefinetheaddLogsfunction,whichwillprintlogstogetherwiththeexecutiontime.Wewilldefineitinthefollowingway:
funaddLogs(tag:String,f:()->Unit){
println("$tagstarted")
valstartTime=System.currentTimeMillis()
f()
valendTime=System.currentTimeMillis()
println("$tagfinished.Ittook"+(endTime-startTime))
}
Thefollowingistheusageofthefunction:
addLogs("Someoperations"){
//Operationswearemeasuring
}
Here'sanexampleofitsexecution:
addLogs("Sleeper"){
Thread.sleep(1000)
}
Onexecutingtheprecedingcode,thefollowingoutputispresented:
Sleeperstarted
Sleeperfinished.Ittook1001
Theexactnumberofprintedmillisecondsmaydifferalittlebit.
ThispatternisreallyusefulinKotlinprojectsbecausesomepatternsareconnectedtoblocksofcode.Forexample,itiscommontocheckwhethertheversionoftheAPIisafterAndroid5.xLollipopbeforetheexecutionoffeaturesthatneedatleastthisversiontowork.Tocheckit,weusedthefollowingcondition:
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP){
//Operations
}
ButinKotlin,wecanjustextractthefunctioninthefollowingway:
funifSupportsLolipop(f:()->Unit){
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP)
{
f()
}
}
//Usage
ifSupportsLollipop{
//Operation
}
Thisisnotonlycomfortable,butalsoitisloweringredundancyinthecode.Thisisoftenreferredasverygoodpractice.Alsonotethatthisconventionallowsustodefinecontrolstructuresthatworkinasimilarwaytostandardones.Wecan,forexample,defineasimplecontrolstructurethatisrunningaslongasthestatementinthebodydoesnotreturnanerror.Hereisthedefinitionandusage:
funrepeatUntilError(code:()->Unit):Throwable{
while(true){
try{
code()
}catch(t:Throwable){
returnt
}
}
}
//Usage
valtooMuchAttemptsError=repeatUntilError{
attemptLogin()
}
Anadditionaladvantageisthatourcustomdatastructurecanreturnavalue.Theimpressivepartisthatisdoesn'tneedanyextralanguagesupport,andwecandefinenearlyanycontrolstructurewewant.
ProcessingdatastructuresusingLINQstyleWe'vealreadymentionedthatKotlinallowsLINQ-styleprocessing.Thelastlambdainargumentconventionisanothercomponentthataidsitsreadability.Forexample,lookatthefollowingcode:
strings.filter{it.length==5}.map{it.toUpperCase()}
Itismorereadablethannotationthatdoesnotusethelastlambdainargumentconvention:
strings.({s->s.length==5}).map({s->s.toUpperCase()})
Again,thisprocessingwillbediscussedindetaillater,inChapter7,ExtensionFunctionsandProperties,butfornowwehavelearnedabouttwofeaturesthatimproveitsreadability(thelastlambdainargumentconventionandtheimplicitnameofasingleparameter).
ThelastlambdainargumentconventionisoneoftheKotlinfeaturesthatwasintroducedtoimprovetheuseoflambdaexpressions.Therearemoresuchimprovements,andhowtheyworktogetherisimportanttomaketheuseofhigher-orderfunctionssimple,readable,andefficient.
JavaSAMsupportinKotlinItisreallyeasytousehigher-orderfunctionsinKotlin.TheproblemisthatweoftenneedtointeroperatewithJava,whichnativelydoesn'tsupportit.Itachievessubstitutionbyusinginterfaceswithonlyonemethod.ThiskindofinterfaceiscalledaSingleAbstractMethod(SAM)orfunctionalinterface.Thebestexampleofsituationinwhichweneedtosetupafunctionthisway,iswhenweareusingsetOnClickListeneronaViewelement.InJava(until8)therewasnosimplerwaythanbyusingananonymousinnerclass:
//Java
button.setOnClickListener(newOnClickListener(){
@OverridepublicvoidonClick(Viewv){
//Operation
}
});
Intheprecedingexample,theOnClickListenermethodistheSAM,becauseitcontainsonlyasinglemethod,onClick.WhileSAMsarereallyoftenusedasareplacementforfunctiondefinitions,Kotlinalsogeneratesaconstructorforthemthatcontainsthefunctiontypeasaparameter.ItiscalledaSAMconstructor.ASAMconstructorallowsustocreateaninstanceofaJavaSAMinterfacejustbycallingitsnameandpassingafunctionliteral.Here'sanexample:
button.setOnClickListener(OnClickListener{
/*...*/
})
Afunctionliteralisanexpressionthatdefinesunnamedfunction.InKotlin,therearetwokindsoffunctionliteral:
1.Anonymousfunctions2.Lambdaexpressions
BothKotlinfunctionliteralhavealreadybeendescribed:
vala=fun(){}//Anonymousfunction
valb={}//Lambdaexpression
Evenbetter,foreachJavamethodthattakesaSAM,theKotlincompilerisgeneratingaversionthatinsteadtakesafunctionasanargument.ThisiswhywecansetOnClickListenerasfollows:
button.setOnClickListener{
//Operations
}
RememberthattheKotlincompilerisgeneratingSAMconstructorsandfunctionmethodsonlyforJavaSAMs.ItisnotgeneratingSAMconstructorsforKotlininterfaceswithasinglemethod.ItisbecausetheKotlincommunityispushingtousefunctiontypesandnotSAMsinKotlincode.WhenafunctioniswritteninKotlinandincludesaSAM,thenwecannotuseitasJavamethodswithSAMonparameter:
interfaceOnClick{
funcall()
}
funsetOnClick(onClick:OnClick){
//...
}
setOnClick{}//1.Error
1. ThisdoesnotworkbecausethesetOnClickfunctioniswritteninKotlin.
InKotlin,interfacesshouldn'tbeusedthisway.ThepreferredwayistousefunctiontypesinsteadofSAMs:
funsetOnClick(onClick:()->Unit){
//...
}
setOnClick{}//Works
TheKotlincompilergeneratesaSAMconstructorforeverySAMinterfacedefinedinJava.ThisinterfaceonlyincludesthefunctiontypethatcansubstituteaSAM.Lookatthefollowinginterface:
//Java,insideViewclass
publicinterfaceOnClickListener{
voidonClick(Viewv);
}
WecanuseitinKotlinthisway:
valonClick=View.OnClickListener{toast("Clicked")}
Orwecanprovideitasfunctionargument:
funaddOnClickListener(d:View.OnClickListener){}
addOnClickListener(View.OnClickListener{v->println(v)})
HerearemoreexamplesoftheJavaSAMlambdainterfaceandmethodsfromAndroid:
view.setOnLongClickListener{/*...*/;true}
view.onFocusChange{view,b->/*...*/}
valcallback=Runnable{/*...*/}
view.postDelayed(callback,1000)
view.removeCallbacks(callback)
Andhere'ssomeexamplesfromRxJava:
observable.doOnNext{/*...*/}
observable.doOnEach{/*...*/}
Now,let'slookathowaKotlinalternativetoSAMdefinitioncanbeimplemented.
NamedKotlinfunctiontypesKotlindoesnotsupportSAMconversionsoftypesdefinedinKotlin,becausethepreferredwayistousefunctiontypesinstead.ButSAMhassomeadvantagesoverclassicfunctiontypes:namedandnamedparameters.Itisgoodtohavethefunctiontypenamedwhenitsdefinitionislongoritispassedmultipletimesasanargument.Itisgoodtohavenamedparameterswhenitisnotclearwhateachparametermeansjustbyitstype.
Intheupcomingsections,wearegoingtoseethatitispossibletonameboththeparametersandthewholedefinitionofafunctiontype.Itcanbedonewithtypealiasesandnamedparametersinthefunctiontype.Thisway,itispossibletohavealltheadvantagesofSAMwhilestickingwithfunctiontypes.
NamedparametersinfunctiontypeUntilnow,we'veonlyseendefinitionsoffunctiontypeswhereonlythetypeswerespecified,butnotparameternames.Parameternameshavebeenspecifiedinfunctionliterals:
funsetOnItemClickListener(listener:(Int,View,View)->Unit){
//code
}
setOnItemClickListener{position,view,parent->/*...*/}
Theproblemcomeswhentheparametersarenotself-explanatory,andthedeveloperdoesnotknowwhattheparametersmean.WithSAMsthereweresuggestions,whileinthefunctiontypedefinedinthepreviousexample,theyarenotreallyhelpful:
Thesolutionistodefinefunctiontypewithnamedparameters.Hereiswhatitlookslike:
(position:Int,view:View,parent:View)->Unit
ThebenefitofthisnotationisthattheIDEsuggeststhesenamesasthenamesoftheparametersinthefunctionliteral.Becauseofthis,programmercanavoidanyconfusion:
Theproblemoccurswhenthesamefunctiontypeisusedmultipletimes,thenitisnoteasytodefinethoseparametersforeachdefinition.Inthatsituation,adifferentKotlinfeatureisused-theonewedescribeinnextsection--typealias.
TypealiasFromversion1.1,Kotlinhashadafeaturecalledtypealias,whichallowsustoprovidealternativenamesforexistingtypes.HereisanexampleofatypealiasdefinitionwherewehavemadealistofUsers:
dataclassUser(valname:String,valsurname:String)
typealiasUsers=List<User>
Thisway,wecanaddmoremeaningfulnamestoexistingdatatypes:
typealiasWeight=Double
typealiasLength=Int
Typealiasesmustbedeclaredatthetoplevel.Avisibilitymodifiercanbeappliedtoatypealiastoadjustitsscope,buttheyarepublicbydefault.Thismeansthatthetypealiasesdefinedpreviouslycanbeusedwithoutanylimitations:
valusers:Users=listOf(
User("Marcin","Moskala"),
User("Igor","Wojda")
)
funcalculatePrice(length:Length){
//...
}
calculatePrice(10)
valweight:Weight=52.0
vallength:Length=34
Keepinmindthataliasesareusedtoimprovecodereadability,andtheoriginaltypescanstillbeusedinterchangably:
typealiasLength=Int
varintLength:Int=17
vallength:Length=intLength
intLength=length
Anotherapplicationoftypealiasistoshortenlonggenerictypesandgivethemmoremeaningfulnames.Thisimprovescodereadabilityandconsistencywhenthesametypeisusedinmultipleplacesinthecode:
typealiasDictionary<V>=Map<String,V>
typealiasArray2D<T>=Array<Array<T>>
Typealiasesareoftenusedtonamefunctiontypes:
typealiasAction<T>=(T)->Unit
typealiasCustomHandler=(Int,String,Any)->Unit
Wecanusethemtogetherwithfunctiontypeparameternames:
typealiasOnElementClicked=(position:Int,view:View,parent:View)->Unit
Andthenwegetparametersuggestions:
Let'slookatanexampleofhowfunctiontypesnamedbytypealiascanbeimplementedbyclass.Parameternamesfromfunctiontypesarealsosuggestedasmethodparametersnamesinthisexample:
typealiasOnElementClicked=(position:Int,view:View,parent:View)->Unit
classMainActivity:Activity(),OnElementClicked{
overridefuninvoke(position:Int,view:View,parent:View){
//code
}
}
Thesearethemainreasonswhyweareusingnamedfunctiontypes:
Namesareoftenshorterandeasierthanwholefunctiontypedefinitions
Whenwearepassingfunctions,afterchangingtheirdefinitions,wedon'thavetochangeiteverywhereifweareusingtypealiasesItiseasiertohavedefinedparameternameswhenweusetypealiases
Thesetwofeatures(namedparameterinfunctiontypesandtypealiases)combinedarethereasonswhythereisnoneedtodefineSAMsinKotlin--alltheadvantagesofSAMsoverfunctiontypes(nameandnamedparameters)canbeachievedwithnamedparametersinfunctiontypedefinitionsandtypealiases.ThisisanotherexampleofhowKotlinsupportsfunctionalprogramming.
UnderscoreforunusedvariablesInsomecases,wearedefiningalambdaexpressionthatdoesnotuseallitsparameters.Whenweleavethemnamed,thentheymightbedestructingaprogrammerwhoisreadingthislambdaexpressionandtryingtounderstanditspurpose.Let'slookatthefunctionthatisfilteringeverysecondelement.Thesecondparameteristheelementvalue,anditisunusedinthisexample:
list.filterIndexed{index,value->index%2==0}
Topreventmisunderstanding,therearesomeconventionsused,suchastheignoringtheparameternames:
list.filterIndexed{index,ignored->index%2==0}
Becausetheseconventionswereunclearandproblematic,Kotlinintroducedunderscorenotation,whichisusedasareplacementforthenamesofparametersthatarenotused:
list.filterIndexed{index,_->index%2==0}
Thisnotationissuggested,andthereisawarningdisplayedwhenalambdaexpressionparameterisunused:
DestructuringinlambdaexpressionsInChapter4,ClassesandObjects,we'veseenhowobjectscanbedestructuredintomultiplepropertiesusingdestructuringdeclarations:
dataclassUser(valname:String,valsurname:String,valphone:String)
val(name,surname,phone)=user
Sinceversion1.1,Kotlincanusedestructuringdeclarationssyntaxforlambdaparameters.Tousethem,youshoulduseparenthesesthatincludealltheparametersthatwewanttodestructureinto:
valshowUser:(User)->Unit={(name,surname,phone)->
println("$name$surnamehavephonenumber:$phone")
}
valuser=User("Marcin","Moskala","+48123456789")
showUser(user)
//MarcinMoskalahavephonenumber:+48123456789
Kotlin'sdestructingdeclarationisposition-based,asopposedtothepropertyname-baseddestructuringdeclarationthatcanbefound,forexample,inTypeScript.Inposition-baseddestructingdeclarations,theorderofpropertiesdecideswhichpropertyisassignedtowhichvariable.Inpropertyname-baseddestructuring,itisdeterminedbythenamesofvariables:
//TypeScript
constobj={first:'Jane',last:'Doe'};
const{last,first}=obj;
console.log(first);//Prints:Jane
console.log(last);//Prints:Doe
Bothsolutionshaveitsprosandcons.Position-baseddestructingdeclarationsaresecuredforrenamingaproperty,buttheyarenotsafeforpropertyreordering.Name-baseddestructuringdeclarationsaresafeforpropertyreorderingbutarevulnerableforpropertyrenaming.
Destructuringdeclarationscanbeusedmultipletimesinasinglelambdaexpression,anditcanbeusedtogetherwithnormalparameters:
valf1:(Pair<Int,String>)->Unit={(first,second)->
/*code*/}//1
valf2:(Int,Pair<Int,String>)->Unit={index,(f,s)->
/*code*/}//2
valf3:(Pair<Int,String>,User)->Unit={(f,s),(name,
surname,tel)->/*code*/}//3
1. DeconstructionofPair2. DeconstructionofPairandotherelement3. Multipledeconstructionsinsinglelambdaexpression
Notethatwecandestructureaclassintolessthanallcomponents:
valf:(User)->Unit={(name,surname)->/*code*/}
Underscorenotationisallowedindestructuringdeclarations.Itismostoftenusedtogettothefurthercomponents:
valf:(User)->Unit={(name,_,phone)->/*code*/}
valthird:(List<Int>)->Int={(_,_,third)->third}
Itispossibletospecifythetypeofthedestructuredparameter:
valf={(name,surname):User->/*code*/}//1
1. Thetypeisinferredfromthelambdaexpression
Also,parametersdefinedbydestructuringdeclaration:
valf={(name:String,surname:String):User->
/*code*/}//1
valf:(User)->Unit={(name,surname)->
/*code*/}//2
1. Thetypeisinferredfromthelambdaexpression.2. Thetypecannotbeinferredbecausethereisnotenoughinformationabout
typesinsidethelambdaexpression.
Thisallmakesdestructuringinlambdasareallyusefulfeature.Let'slookatsomemostcommonusecasesinAndroidwheredeconstructioninlambdasisused.ItisusedtoprocesstheelementsofMapbecausetheyareoftypeMap.Entry,whichcanbedestructedtothekeyandvalueparameters:
valmap=mapOf(1to2,2to"A")
valtext=map.map{(key,value)->"$key:$value"}
println(text)//Prints:[1:2,2:A]
Similarly,listsofpairscanbedestructed:
vallistOfPairs=listOf(1to2,2to"A")
valtext=listOfPairs.map{(first,second)->
"$firstand$second"}
println(text)//Prints:[1and2,2andA]
Destructuringdeclarationsarealsousedwhenwewanttosimplifydataobjectsprocessing:
funsetOnUserClickedListener(listener:(User)->Unit){
listView.setOnItemClickListener{_,_,position,_->
listener(users[position])
}
}
setOnUserClickedListener{(name,surname)->
toast("Clickedto$name$surname")
}
Thisisespeciallyusefulinlibrariesthatareusedtoasynchronouslyprocesselements(suchasRxJava).Theirfunctionsaredesignedtoprocesssingleelements,andifwewantmultipleelementstobeprocessed,thenweneedtopacktheminPair,Triple,orsomeotherdataclassanduseadestructuringdeclarationoneachstep:
getQuestionAndAnswer()
.flatMap{(question,answer)->
view.showCorrectAnswerAnimationObservable(question,answer)
}
.subscribe({(question,answer)->/*code*/})
InlinefunctionsHigher-orderfunctionsareveryhelpfulandtheycanreallyimprovethereusabilityofcode.However,oneofthebiggestconcernsaboutusingthemisefficiency.Lambdaexpressionarecompiledtoclasses(oftenanonymousclasses),andobjectcreationinJavaisaheavyoperation.Wecanstillusehigher-orderfunctionsinaneffectivewaywhilekeepingallthebenefitsbymakingfunctionsinline.
Theconceptofinlinefunctionsisprettyold,anditismostlyrelatedtoC++orC.Whenafunctionismarkedasinline,duringcodecompilationthecompilerwillreplaceallthefunctioncallswiththeactualbodyofthefunction.Also,lambdaexpressionsprovidedasargumentsarereplacedwiththeiractualbody.Theywillnotbetreatedasfunctions,butasactualcode.Thisismakesbytecodelonger,butruntimeexecutionismuchmoreefficient.Later,wewillseethatnearlyallhigher-orderfunctionsfromstandardlibraryaremarkedasinline.Let'slookattheexample.SupposewemarkedtheprintExecutionTimefunctionwiththeinlinemodifier:
inlinefunprintExecutionTime(f:()->Unit){
valstartTime=System.currentTimeMillis()
f()
valendTime=System.currentTimeMillis()
println("Ittook"+(endTime-startTime))
}
funmeasureOperation(){
printExecutionTime{
longOperation()
}
}
WhenwecompileanddecompilemeasureOperation,wearegoingtofindoutthatthefunctioncallisreplacedwithitsactualbody,andtheparameterfunctioncallisreplacedbythelambdaexpression'sbody:
funmeasureOperation(){
valstartTime=System.currentTimeMillis()//1
longOperation()//2
valendTime=System.currentTimeMillis()
println("Ittook"+(endTime-startTime))
}
1. CodefromprintExecutionTimewasaddedtomeasureOperationfunctionbody.2. Codelocatedinsidethelambdawaslocatedonitscall.Ifthefunctionused
itmultipletimes,thenthecodewouldreplaceeachcall.
ThebodyofprintExecutionTimecanstillbefoundinthecode.Itwasskippedtomaketheexamplemorereadable.Itiskeptinthecodebecauseitmightbeusedaftercompilation,forexample,ifthiscodeisaddedtoaprojectasalibrary.Whatismore,thisfunctionwillstillworkasinlinewhenusedbyKotlin.
Whilethereisnoneedtocreateclassesforlambdaexpressions,inlinefunctionscanspeeduptheexecutionoffunctionswithfunctionparameters.Thisdifferenceissoimportantthatitisrecommendedtousetheinlinemodifierforallshortfunctionswithatleastonefunctionparameter.Unfortunately,usingtheinlinemodifieralsohasitsbadsides.Thefirst,we'vealreadymentioned--theproducedbytecodeislonger.Thisisbecausefunctioncallsarereplacedbyfunctionbodiesandbecauselambdacallsinsidethisbodyarereplacedwiththebodyofthefunctionliteral.Also,inlinefunctionscannotberecursiveandtheycannotusefunctionsorclassesthathavemorerestrictivevisibilitymodifierthanthislambdaexpression.Forexample,publicinlinefunctionscannotuseprivatefunctions.Thereasonisthatitcouldleadtotheinjectionofcodeintofunctionsthatcannotusethem.Thiswouldleadtoacompilationerror.Topreventit,Kotlindoesnotpermittheuseofelementswithlessrestrictivemodifiersthanthelambdaexpressioninwhichtheyareplaced.Here'sanexample:
internalfunsomeFun(){}
inlinefuninlineFun(){
someFun()//ERROR
}
Infact,itispossibleinKotlintouseelementswithmorerestrictivevisibilityininlinefunctionsifwesuppressthiswarning,butthisisbadpracticeanditshouldneverbeusedthisway:
//Tester1.kt
funmain(args:Array<String>){a()}
//Tester2.kt
inlinefuna(){b()}
privatefunb(){print("B")}
Howisitpossible?Fortheinternalmodifieritissimpler,because
theinternalmodifierispublicunderthehood.Forprivatefunctions,thereisanadditionalaccess$bfunctioncreatedthathaspublicvisibilityandthatisonlyinvokingthebfunction:
publicstaticfinalvoidaccess$b(){b();}
Thisbehaviorispresentedherejusttoexplainwhylessrestrictivemodifierscansometimesbeusedinsideinlinefunctions(thesesituationscanbefoundinKotlinstandardlibraryinKotlin1.1).Intheprojects,weshoulddesignelementsinsuchawaythatthereisnoneedtousesuchsuppressions.
Anotherproblemislessintuitive.Whilenolambdahasbeencreated,wecannotpassparametersthatareofthefunctiontypetoanotherfunction.Hereisanexample:
funboo(f:()->Int){
//...
}
inlinefunfoo(f:()->Int){
boo(f)//ERROR,1
}
Whenfunctionisinline,thenitsfunctionargumentscannotbepassedtofunctionthatarenotinline.
Thisdoesn'tworkbecausenofparameterhasbeencreated.Ithasjustbeendefinedtobereplacedbythefunctionliteralbody.Thisiswhyitcannotbepassedtoanotherfunctionasanargument.
Thesimplestwaytodealwithitisbymakingtheboofunctioninlineaswell.ThenitwillbeOK.Inmostcases,wecannotmaketoomanyfunctionsinline.Hereareafewreasonswhy:
Theinlinefunctionsshouldbeusedforsmallerfunctions.Ifwearemakinginlinefunctionsthatareusingotherinlinefunctions,thenitcanleadtoalargestructurebeinggeneratedaftercompilation.Thisisaproblembothbecauseofcompilationtimeandbecauseoftheresultingcode'ssize.Whileinlinefunctionscannotuseelementwithvisibilitymodifiersmorestrictthantheonetheyhave,itwouldbeaproblemifwewouldliketouse
theminlibrarieswhereasmanyfunctionsaspossibleshouldbeprivatetoprotecttheAPI.
Thesimplestwaytodealwiththisproblemisbymakingfunctionparametersthatwedowanttopasstoanotherfunctionnoinline.
ThenoinlinemodifierThenoinlineisamodifierforfunctiontypeparameters.Itmakesaspecificargumenttreatedasnormalfunctiontypeparameter(itscallsarenotreplacedwiththefunctionliteralbody).Let'slookatanoinlineexample:
funboo(f:()->Unit){
//...
}
inlinefunfoo(before:()->Unit,noinlinef:()->Unit){//1
before()//2
boo(f)//3
}
1. Thenoinlineannotationmodifierbeforeparameterf.2. Thebeforefunctionwillbereplacedbythebodyofthelambdaexpression
usedasanargument.3. fisnoinlinesoitcanbepassedtotheboofunction.
Twomainreasonstousenoinlinemodifierareasfollows:
WhenweneedtopassaspecificlambdatosomeotherfunctionWhenwearecallingthelambdaintensivelyandwedon'twanttoswellthecodetoomuch
Notethatwhenwemakeallfunctionparametersnoinline,thentherewillbenearlynoperformanceimprovementfrommakingthefunctionsinline.Whileitisunlikelythatusinginlinewillbebeneficial,thecompilerwillshowawarning.Thisiswhy,inmostcases,noinlineisonlyusedwhentherearemultiplefunctionparametersandweonlyapplyittosomeofthem.
Non-localreturnsFunctionswithfunctionparametersmightactsimilarlytonativestructures(suchasloops).We'vealreadyseentheifSupportsLolipopfunctionandtherepeatUntilErrorfunction.AnevenmorecommonexampleistheforEachmodifier.Itisanalternativetotheforcontrolstructure,anditcallsaparameterfunctionwitheachelementoneafteranother.Thisishowitcouldbeimplemented(thereisaforEachmodifierinKotlinstandardlibrary,butwewillseeitlaterbecauseitincludeselementsthathavenotyetbeenpresented):
funforEach(list:List<Int>,body:(Int)->Unit){
for(iinlist)body(i)
}
//Usage
vallist=listOf(1,2,3,4,5)
forEach(list){print(it)}//Prints:12345
ThebigproblemisthatinsidetheforEachfunctiondefinedthiswaywecannotreturnfromouterfunction.Forexample,thisishowwecouldimplementthemaxBoundedfunctionusingaforloop:
funmaxBounded(list:List<Int>,upperBound:Int,lowerBound:Int):
Int{
varcurrentMax=lowerBound
for(iinlist){
when{
i>upperBound->returnupperBound
i>currentMax->currentMax=i
}
}
returncurrentMax
}
IfwewanttotreatforEachasanalternativetoaforloop,thensimilarpossibilityshouldbeallowedthere.Theproblemisthatthesamecode,butwithforEachusedinsteadofforloop,wouldnotcompile:
Thereasonisrelatedtohowthecodeiscompiled.Wehavealreadydiscussedthatlambdaexpressionsarecompiledtoclassofanonymousobjectswithamethodthatincludesthedefinedcode,andovertherewecannotreturnfromthemaxBoundedfunctionbecauseweareinadifferentcontext.
WeencounterasituationwhentheforEachfunctionismarkedasinline.Aswehavealreadymentioned,thebodyofthisfunctionreplacesitscallsduringcompilation,andallofthefunctionsfromtheparametersarereplacedwiththeirbody.So,thereisnoproblemwithusingthereturnmodifierthere.Then,ifwemakeforEachinline,wecanusereturninsidethelambdaexpression:
inlinefunforEach(list:List<Int>,body:(Int)->Unit){
for(iinlist)body(i)
}
funmaxBounded(list:List<Int>,upperBound:Int,
lowerBound:Int):Int{
varcurrentMax=lowerBound
forEach(list){i->
when{
i>upperBound->returnupperBound
i>currentMax->currentMax=i
}
}
returncurrentMax
}
ThisishowthemaxBoundedfunctionhascompiledinKotlin,andthecodelookslikethis(aftersomeclean-upandsimplification)whenitisdecompiledtoJava:
publicstaticfinalintmaxBounded(@NotNullListlist,
intupperBound,intlowerBound){
intcurrentMax=lowerBound;
Iteratoriter=list.iterator();
while(iter.hasNext()){
inti=((Number)iter.next()).intValue();
if(i>upperBound){
returnupperBound;//1
}
if(i>currentMax){
currentMax=i;
}
}
returncurrentMax;
}
Intheprecedingcode,returnisimportant--itwasdefinedinthelambdaexpression,anditisreturningfromthemaxBoundedfunction.
Thereturnmodifierusedinsidethelambdaexpressionoftheinlinefunctioniscalledanon-localreturn.
LabeledreturninlambdaexpressionsLet'slookatacaseinwhichweneedtoreturnfromalambdaexpressionandnotfromafunction.Wecandothisusinglabels.Hereisanexampleofareturnfromalambdaexpressionusinglabels:
inlinefun<T>forEach(list:List<T>,body:(T)->Unit){//1
for(iinlist)body(i)
}
funprintMessageButNotError(messages:List<String>){
forEach(messages)messageProcessor@{//2
if(it=="ERROR")return@messageProcessor//3
print(it)
}
}
//Usage
vallist=listOf("A","ERROR","B","ERROR","C")
processMessageButNotError(list)//Prints:ABC
1. ThisisgenericimplementationofforEachfunction,wherelistwithanytypecanbeprocessed.
2. WedefinelabelforlambdaexpressioninsideforEachargument.3. Wereturnfromlambdaexpressionspecifiedbylabel.
AnotherKotlinfeatureisthatlambdaexpressionsthataredefinedasfunctionargumentshaveadefaultlabelwhosenameisthesameasthefunctioninwhichtheyaredefined.Thislabeliscalledanimplicitlabel.WhenwewanttoreturnfromalambdaexpressiondefinedinaforEachfunction,wecandoitjustbyusingreturn@forEach.Let'slookatanexample:
inlinefun<T>forEach(list:List<T>,body:(T)->Unit){//1
for(iinlist)body(i)
}
funprocessMessageButNotError(messages:List<String>){
forEach(messages){
if(it=="ERROR")return@forEach//1
process(it)
}
}
//Usage
vallist=listOf("A","ERROR","B","ERROR","C")
processMessageButNotError(list)//Prints:ABC
1. Implicitlabelnameistakenfromfunctionname.
NotethatwhiletheforEachfunctionisinline,wecanalsouseanon-localreturntoreturnfromtheprocessMessageButNotErrorfunction:
inlinefun<T>forEach(list:List<T>,body:(T)->Unit){
for(iinlist)body(i)
}
funprocessMessageButNotError(messages:List<String>){
forEach(messages){
if(it=="ERROR")return
process(it)
}
}
//Usage
vallist=listOf("A","ERROR","B","ERROR","C")
processMessageButNotError(list)//Prints:A
Let'smoveontoamorecomplexexampleofusingnon-localreturnlabels.Let'ssupposethatwehavetwoforEachloops,oneinsideanother.Whenweuseanimplicitlabel,itwillreturnfromthedeeperloop.Inourexample,wecanuseittoskiptheprocessingofthespecificmessage:
inlinefun<T>forEach(list:List<T>,body:(T)->Unit){//1
for(iinlist)body(i)
}
funprocessMessageButNotError(conversations:List<List<String>>){
forEach(conversations){messages->
forEach(messages){
if(it=="ERROR")return@forEach//1.
process(it)
}
}
}
//Usage
valconversations=listOf(
listOf("A","ERROR","B"),
listOf("ERROR","C"),
listOf("D")
)
processMessageButNotError(conversations)//ABCD
1. ThiswillreturnfromthelambdadefinedintheforEachfunctionthatalsotakesmessagesasanargument.
Wecannotreturnfromanotherlambdaexpressioninthesamecontextusingimplicitlabel,becauseitisshadowedbyadeeperimplicitlabel.
Inthesesituations,weneedtouseanon-localimplicitlabelreturn.Itisonlypermissiblewithinlinefunctionparameters.Inourexample,whileforEachisinline,wecanreturnfromafunctionliteralthisway:
inlinefun<T>forEach(list:List<T>,body:(T)->Unit){//1
for(iinlist)body(i)
}
funprocessMessageButNotError(conversations:List<List<String>>){
forEach(conversations)conv@{messages->
forEach(messages){
if(it=="ERROR")return@conv//1.
print(it)
}
}
}
//Usage
valconversations=listOf(
listOf("A","ERROR","B"),
listOf("ERROR","C"),
listOf("D")
)
processMessageButNotError(conversations)//AD
1. ThiswillreturnfromthelambdadefinedinforEachcalledonconversations.
Wecanalsojustuseanon-localreturn(areturnwithoutanylabels)tofinishtheprocessing:
inlinefun<T>forEach(list:List<T>,body:(T)->Unit){//1
for(iinlist)body(i)
}
funprocessMessageButNotError(conversations:List<List<String>>){
forEach(conversations){messages->
forEach(messages){
if(it=="ERROR")return//1.
process(it)
}
}
}
1. ThiswillreturnfromtheprocessMessageButNotErrorfunctionandfinishtheprocessing.
CrossinlinemodifierSometimesweneedtousefunctiontypeparametersfrominlinefunctionsnotdirectlyinthefunctionbody,butinanotherexecutioncontext,suchasalocalobjectoranestedfunction.Butstandardfunctiontypeparametersofinlinefunctionsarenotallowedtobeusedthisway,becausetheyareallowingnon-localreturns,anditshouldnotbeallowedifthisfunctioncouldbeusedinsideanotherexecutioncontext.Toinformthecompilerthatnon-localreturnsarenotallowed,thisparametermustbeannotatedascrossinline.Thenitwillactlikeasubstitutionthatweareexpectinginaninlinefunction,evenwhenitisusedinsideanotherlambdaexpression:
funboo(f:()->Unit){
//...
}
inlinefunfoo(crossinlinef:()->Unit){
boo{println("A");f()}
}
funmain(args:Array<String>){
foo{println("B")}
}
Thiswillbecompiledasfollows:
funmain(args:Array<String>){
boo{println("A");println("B")}
}
Whilenopropertyhasbeencreatedwiththefunction,itisnotpossibletopassthecrossinlineparametertoanotherfunctionasanargument:
Let'slookatapracticalexample.InAndroid,wedon'tneedContexttoexecuteanoperationonthemainthreadoftheapplicationbecausewecangetamainloopusingthegetMainLooperstaticfunctionfromtheLooperclass.Therefore,wecanwriteatop-levelfunctionthatwillallowasimplethreadchangeintothemainthread.Tooptimizeit,wearefirstcheckingifthecurrentthreadisnotthemain
thread.Whenitis,thentheactionisjustinvoked.Whenitisnot,thenwecreateahandlerthatoperatesonthemainthreadandapostoperationtoinvokeitfromthere.Tomaketheexecutionofthisfunctionfaster,wearegoingtomaketherunOnUiThreadfunctioninline,butthentoallowtheactioninvocationfromanotherthread,weneedtomakeitcrossinline.Hereisanimplementationofthisdescribedfunction:
inlinefunrunOnUiThread(crossinlineaction:()->Unit){
valmainLooper=Looper.getMainLooper()
if(Looper.myLooper()==mainLooper){
action()
}else{
Handler(mainLooper).post{action()}//1
}
}
1. Wecanrunactioninsidealambdaexpressionthankstothecrossinlinemodifier.
Thecrossinlineannotationisusefulbecauseitallowstousefunctiontypesinthecontextoflambdaexpressionsorlocalfunctionswhilemaintainingtheadvantagesofmakingthefunctioninline(there'snoneedforlambdacreationinthiscontext).
InlinepropertiesSinceKotlin1.1,theinlinemodifiercanbeusedonpropertiesthatdonothaveabackingfield.Itcanbeeitherappliedtoseparateaccessors,whichwillresultintheirbodyreplacingusage,oritcanbeusedforwholeproperty,whichwillhavethesameresultasmakingbothaccessorsinline.Let'smakeaninlinepropertythatwillbeusedtocheckandchangeanelement'svisibility.Hereisanimplementationwherebothaccessorsareinline:
varviewIsVisible:Boolean
inlineget()=findViewById(R.id.view).visibility==View.VISIBLE
inlineset(value){
findViewById(R.id.view).visibility=if(value)View.VISIBLE
elseView.GONE
}
Wecanachievethesameresultifweannotatethewholepropertyasinline:
inlinevarviewIsVisible:Boolean
get()=findViewById(R.id.view).visibility==View.VISIBLE
set(value){
indViewById(R.id.view).visibility=if(value)View.VISIBLE
elseView.GONE
}
//Usage
if(!viewIsVisible)
viewIsVisible=true
Theprecedingcodebecompiledasfollows:
if(!(findViewById(R.id.view).getVisibility()==View.VISIBLE))
{
findViewById(R.id.view).setVisibility(true?View.VISIBLE:View.GONE);
}
Thisway,wehaveomittedthesetterandgetterfunctioncalls,andweshouldexpectaperformanceimprovementwiththecostofincreasedcompiledcodesize.Still,formostproperties,itshouldbeprofitabletousetheinlinemodifier.
FunctionReferencesSometimes,functionsthatwewanttopassasanargumentarealreadydefinedasaseparatefunction.Thenwecanjustdefinethelambdawithitscall:
funisOdd(i:Int)=i%2==1
list.filter{isOdd(it)}
ButKotlinalsoallowsustopassafunctionasavalue.Tobeabletouseatop-levelfunctionasavalue,weneedtouseafunctionreference,whichisusedasadoublecolonandthefunctionname(::functionName).Hereisanexamplehowitcanbeusedtoprovideapredicatetofilter:
list.filter(::isOdd)
Hereisanexample:
fungreet(){
print("Hello!")
}
funsalute(){
print("Haveaniceday")
}
valtodoList:List<()->Unit>=listOf(::greet,::salute)
for(taskintodoList){
task()
}
//Prints:Hello!Haveaniceday
Functionreferenceisexampleofreflection,andthisiswhytheobjectreturnedbythisoperationalsocontainsinformationaboutthereferredfunction:
funisOdd(i:Int)=i%2==1
valannotations=::isOdd.annotations
valparameters=::isOdd.parameters
println(annotations.size)//Prints:0
println(parameters.size)//Prints:1
Butthisobjectalsoimplementsthefunctiontype,anditcanbeusedthisway:
valpredicate:(Int)->Boolean=::isOdd
Itisalsopossibletoreferencetomethods.Todoit,weneedtowritethetypename,twocolons,andthemethodname(Type::functionName).Hereisanexample:
valisStringEmpty:(String)->Boolean=String::isEmpty
//Usage
valnonEmpty=listOf("A","","B","")
.filter(String::isNotEmpty)
print(nonEmpty)//Prints:["A","B"]
Asintheprecedingexample,whenwearereferencinganon-staticmethod,thereneedstobeaprovidedinstanceoftheclassasanargument.TheisEmptyfunctionisaStringmethodthattakesnoarguments.ThereferencetoisEmptyhasaStringparameterthatwillbeusedasanobjectonwhichthefunctionisinvoked.Thereferencetotheobjectisalwayslocatedasthefirstparameter.Hereisanotherexample,wherethemethodhasthepropertyfoodalreadydefined:
classUser{
funwantToEat(food:Food):Boolean{
//...
}
}
valfunc:(User,Food)->Boolean=User::wantToEat
ThereisadifferentsituationwhenwearereferencingaJavastaticmethod,becauseitdoesnotneedinstanceoftheclassonwhichitisdefined.Thisissimilartomethodsofobjectsorcompanionobjects,wheretheobjectisknowninadvanceanddoesnotneedtobeprovided.Inthesesituations,thereisafunctioncreatedwiththesameparametersasthereferencedfunctionandthesamereturntype:
objectMathHelpers{
funisEven(i:Int)=i%2==0
}
classMath{
companionobject{
funisOdd(i:Int)=i%2==1
}
}
//Usage
valevenPredicate:(Int)->Boolean=MathHelpers::isEven
valoddPredicate:(Int)->Boolean=Math.Companion::isOdd
valnumbers=1..10
valeven=numbers.filter(evenPredicate)
valodd=numbers.filter(oddPredicate)
println(even)//Prints:[2,4,6,8,10]
println(odd)//Prints:[1,3,5,7,9]
Infunctionreferenceusage,therearecommonusecaseswherewewanttousefunctionreferencestoprovidemethodfromaclasswehavereferenceto.Commonexampleiswhenwewanttoextractsomeoperationsasmethodofthesameclass,orwhenwewanttoreferencetofunctionsfromreferencememberfunctionfromclasswehavereferenceto.Asimpleexampleiswhenwedefinewhatshouldbedoneafteranetworkoperation.ItisdefinedinaPresenter(suchasMainPresenter),butitisreferencingalltheViewoperations,thataredefinedbytheviewproperty(whichis,forexample,oftypeMainView):
getUsers().smartSubscribe(
onStart={view.showProgress()},//1
onNext={user->onUsersLoaded(user)},//2
onError={view.displayError(it)},//1
onFinish={view.hideProgress()}//1
)
1. showProgress,displayError,andhideProgressaredefinedinMainView.2. onUsersLoadedismethoddefinedinMainPresenter.
Tohelpinthiskindofsituation,Kotlinintroducedinversion1.1featurecalledboundreferences,whichprovidereferencesthatareboundtoaspecificobject.Thankstothat,thisobjectdoesnotneedtobeprovidedbyanargument.Usingthisnotation,wecanreplacethepreviousdefinitionthisway:
getUsers().smartSubscribe(
onStart=view::showProgress,
onNext=this::onUsersLoaded,
onError=view::displayError,
onFinish=view::hideProgress
)
Anotherfunctionthatwemightwanttoreferenceisaconstructor.Anexampleusecaseiswhenweneedtomapfromadatatransferobject(DTO)toaclassthatispartofamodel:
funtoUsers(usersDto:List<UserDto>)=usersDto.map{User(it)}
Here,UserneedstohaveaconstructorthatdefineshowitisconstructedfromUserDto.
ADTOisanobjectthatcarriesdatabetweenprocesses.Itisusedbecauseclassesusedduringcommunicationsbetweenasystem(inanAPI)aredifferentthanactualclassesusedinsidethesystem(amodel).
InKotlin,constructorsareusedandtreatedsimilarlytofunctions.Wecanalsoreferencetothemwithadoublecolonandaclassname:
valmapper:(UserDto)->User=::User
Thisway,wecanreplacethelambdawithaconstructorcallwithaconstructorreference:
funtoUsers(usersDto:List<UserDto>)=usersDto.map(::User)
Usingfunctionreferencesinsteadoflambdaexpressionsgivesusshorterandoftenmorereadablenotation.Itisalsoespeciallyusefulwhenwearepassingmultiplefunctionsasparameters,orfunctionsthatarelongandneedtobeextracted.Inothercases,thereistheusefulboundedreference,whichprovidesareferencethatisboundtoaspecificobject.
SummaryInthischapter,we'vediscussedusingfunctionsasfirst-classcitizens.We'veseenhowfunctiontypesareused.Wehaveseenhowtodefinefunctionliterals(anonymousfunctionsandlambdaexpressions),andthatanyfunctioncanbeusedasanobjectthankstofunctionreferences.We'vealsodiscussedhigher-orderfunctionsanddifferentKotlinfeaturesthatsupportthem:theimplicitnameofasingleparameter,thelastlambdainargumentconvention,JavaSAMsupport,usinganunderscoreforunusedvariables,anddestructuringdeclarationsinlambdaexpressions.Thisfeaturesprovidegreatsupportforhigher-orderfunctions,andtheymakefunctionsevenmorethanfirst-classcitizens.
Inthenextchapter,wearegoingtoseehowgenericsworkinKotlin.Thiswillallowustodefinemuchmorepowerfulclassesandfunctions.Wewillalsoseehowwelltheycanbeusedwhenconnectedtohigher-orderfunctions.
GenericsAreYourFriendsInthepreviouschapter,wediscussedconceptsrelatedtofunctionalprogrammingandfunctionsasfirst-classcitizensinKotlin.
Inthischapter,wewilldiscussconceptofgenerictypesandgenericfunctionsknownasgenerics.Wewilllearnwhytheyexistandhowtousethem-wewilldefinegenericclasses,interfaces,andfunctions.Wewilldiscusshowtodealwithgenericsatruntime,takelookatsubtypingrelations,anddealwithgenericsnullability
Inthischapter,wewilldiscusstheconceptsofgenerictypesandgenericfunctions,knownasgenerics.Wewilllearnwhytheyexistandhowtousethemandalsohowtodefinegenericclasses,interfaces,andfunctions.Wewilldiscusshowtodealwithgenericsatruntime,takealookatsubtypingrelations,anddealwithgenericnullability.
Inthischapter,wewillcoverthefollowingtopics:
GenericclassesGenericinterfacesGenericfunctionsGenericconstraintsGenericnullabilityVarianceUse-sitetargetversusdeclaration-sitetargetDeclaration-sitetargetTypeerasureReifiedanderasedtypeparametersStar-projectionsyntaxVariance
GenericsGenericisaprogrammingstylewhereclasses,functions,datastructures,oralgorithmsarewritteninsuchawaythattheexacttypecanbespecifiedlater.Ingeneral,genericsprovidetypesafetytogetherwiththeabilitytoreuseaparticularcodestructureforvariousdatatypes.
GenericsarepresentinbothJavaandKotlin.Theyworkinasimilarway,butKotlinoffersafewimprovementsovertheJavagenerictypesystem,suchasuse-sitevariance,start-projectionsyntax,andreifiedtypeparameters.Wewilldiscusstheminthischapter.
TheneedforgenericsProgrammersoftenneedawaytospecifythatacollectioncontainsonlyelementsofparticulartype,suchasInt,Student,orCar.Withoutgenerics,wewouldneedseparateclassesforeachdatatype(IntList,StudentList,CarList,andsoon).Thoseclasseswouldhaveaverysimilarinternalimplementation,whichwouldonlydifferinthestoreddatatype.Thismeansthatwewouldneedtowritethesamecode(suchasaddingorremovinganitemfromacollection)multipletimesandmaintaineachclassseparately.Thisisalotofwork,sobeforegenericswereimplemented,programmersusuallyoperatedonauniversallist.Thisforcedthemtocastelementseachtimetheywereaccessed:
//Java
ArrayListlist=newArrayList();
list.add(1);
list.add(2);
intfirst=(int)list.get(0);
intsecond=(int)list.get(1);
Castingaddsboilerplate,andthereisnotypevalidationwhenanelementisaddedtoacollection.Genericsarethesolutionforthisproblem,becauseagenericclassdefinesandusesaplaceholderinsteadofarealtype.Thisplaceholderiscalledatypeparameter.Let'sdefineourfirstgenericclass:
classSimpleList<T>//Tistypeparameter
Thetypeparametermeansthatourclasswilluseacertaintype,butthistypewillbespecifiedduringclasscreation.Thisway,ourSimpleListclasscanbeinstantiatedforavarietyoftypes.Wecanparametrizeagenericclasswithvariousdatatypesusingtypearguments.Thisallowsustocreatemultipledatatypesfromsingleclass:
//Usage
varintList:SimpleList<Int>
varstudentList:SimpleList<Student>
varcarList:SimpleList<Car>
TheSimpleListclassisparametrizedwithtypearguments(Int,Student,andCar)thatdefinewhatkindofdatacanbestoredinthegivenlist.
TypeparametersversustypeargumentsFunctionshaveparameters(variablesdeclaredinsideafunctiondeclaration)andarguments(actualvaluethatispassedtoafunction).Similarterminologyappliesforgenerics.Atypeparameterisablueprintorplaceholderforatypedeclaredinagenericandatypeargumentisanactualtypeusedtoparametrizeageneric.
Wecanuseatypeparameterinamethodsignature.Thisway,wecanmakesurethatwewillbeabletoadditemsofacertaintypetoourlistandretrieveitemsofacertaintype:
classSimpleList<T>{
funadd(item:T){//1
//code
}
funget(intex:Int):T{//2
//code
}
}
1. GenerictypeparameterTusedastypeforitem2. Typeparameterusedasreturntype
Thetypeofitemthatcanbeaddedtoalistorretrievedfromalistdependsonthetypeargument.Let'sseeanexample:
classStudent(valname:String)
valstudentList=SimpleList<Student>()
studentList.add(Student("Ted"))
println(studentList.getItemAt(0).name)
WecanonlyaddandgetitemsoftypeStudentfromthelist.Thecompilerwillautomaticallyperformallnecessarytypechecks.Itisguaranteedthatthecollectionwillonlycontainobjectsofaparticulartype.Passinganobjectofincompatibletypetotheaddmethodwillresultinacompile-timeerror:
varstudentList:SimpleList<Student>
studentList.add(Student("Ted"))
studentList.add(true)//error
WecannotaddBoolean,becauseexpectedtypeisStudent.
TheKotlinstandardlibrarydefinesvariousgenericcollectionsinthekotlin.collectionspackage,suchasList,Set,andMap.WewilldiscusstheminChapter7,ExtensionFunctionsandProperties.
InKotlin,genericsareoftenusedincombinationwithhigher-orderfunctions(discussedinChapter5,FunctionsasAFirstClassCitizen)andextensionfunctions(whichwewilldiscussinChapter7,ExtensionFunctionsandProperties).Examplesofsuchconnectionsarefunctions:map,filter,takeUntil,andsoon.Wecanperformcommonoperationsthatwilldifferinthedetails.Forexample,wecanfindmatchingelementsinthecollectionusingtheoperationfilterfunctionandspecifyinghowmatchingelementswillbedetected:
valfruits=listOf("Babana","Orange","Apple","Blueberry")
valbFruits=fruits.filter{it.startsWith("B")}//1
println(bFruits)//Prints:[Babana,Blueberry]
1. WecancallthestartsWithmethod,becausethecollectioncancontainonlyStrings,sothelambdaparameter(it)hasthesametype.
GenericconstraintsBydefault,wecanparametrizeagenericclasswithanytypeoftypeargument.However,wecanlimitthepossibletypesthatcanbeusedastypearguments.Tolimitthepossiblevaluesoftypeargument,weneedtodefineatypeparameterbound.Themostcommontypeofconstraintisanupperbound.Bydefault,alltypeparametershaveAny?asanimplicitupperbound.Thisiswhyboththefollowingdeclarationsareequivalent:
classSimpleList<T>
classSimpleList<T:Any?>
TheprecedingboundsmeanthatwecanuseanytypewewantastypeargumentforourSimpleListclass(includingnullabletypes).Thisispossiblebecauseallnullableandnon-nullabletypesaresubtypesofAny?:
classSimpleList<T>
classStudent
//usage
varintList=SimpleList<Int>()
varstudentList=SimpleList<Student>()
varcarList=SimpleList<Boolean>()
Insomesituations,wewanttolimitthedatatypesthatcanbeusedastypearguments.Tomakeithappen,weneedtoexplicitlydefineatypeparameterupperbound.Let'sassumethatwewanttobeabletouseonlynumerictypesastypeargumentsforourSimpleListclass:
classSimpleList<T:Number>
//usage
varnumberList=SimpleList<Number>()
varintList=SimpleList<Int>()
vardoubleList=SimpleList<Double>()
varstringList=SimpleList<String>()//error
TheNumberclassisanabstractclass,thatis,asuperclassofKotlinnumerictypes(Byte,Short,Int,Long,Float,andDouble).WecanusetheNumberclassandallitssubclasses(Int,Double,andsoon)asatypeargument,butwecan'tusetheStringclass,becauseit'snotasubclassofNumber.AnyattempttoaddanincompatibletypewillberejectedbytheIDEandcompiler.Typeparametersalsoincorporate
Kotlintypesystemnullability.
NullabilityWhenwedefineaclasswithanunboundedtypeparameter,wecanusebothnon-nullableandnullabletypesastypearguments.Occasionally,weneedtomakesurethataparticulargenerictypewillnotbeparametrizedwithnullabletypearguments.Toblocktheabilitytousenullabletypesastypearguments,weneedtoexplicitlydefineanon-nullabletypeparameterupperbound:
classAction(valname:String)
classActionGroup<T:Action>
//non-nullabletypeparameterupperbound
varactionGroupA:ActionGroup<Action>
varactionGroupB:ActionGroup<Action?>//Error
Nowwecan'tpassanullabletypeargument(Action?)totheActionGroupclass.
Let'sconsideranotherexample.ImaginethatwewanttoretrievethelastActionintheActionGroup.Asimpledefinitionofthelastmethodwouldlooklikethis:
classActionGroup<T:Action>(privatevallist:List<T>){
funlast():T=list.last()
}
Let'sanalyzewhatwillhappenwhenwepassanemptylisttotheconstructor:
valactionGroup=ActionGroup<Action>(listOf())
//...
valaction=actionGroup.last
//error:NoSuchElementException:Listisempty
println(action.name)
Ourapplicationcrashes,becausethemethodlastisthrowinganerrorwhenthereisnoelementwithsuchanindexonthelist.Insteadofanexception,wemightpreferanullvaluewhenthelistisempty.TheKotlinstandardlibraryalreadyhasacorrespondingmethodthatwillreturnanullvalue:
classActionGroup<T:Action>(privatevallist:List<T>){
funlastOrNull():T=list.lastOrNull()//error
}
Thecodewillnotcompilebecausethereisapossibilitythatthelastmethodwill
returnnullirrespectiveoftypeargumentnullability(theremaybenoelementsinthelisttoreturn).Tosolvethisproblem,weneedtoenforceanullablereturntypebyaddingaquestionmarktothetypeparameteruse-site(T?):
classActionGroup<T:Action>(privatevallist:List<T>){//1
funlastOrNull():T?=list.lastOrNull()//2
}
1. Typeparameterdeclaration-site(placeincodewheretypeparameterisdeclared)
2. Typeparameteruse-site(placeincodewheretypeparameterisused)
TheT?parametermeansthatthelastOrNullmethodwillalwaysbenullableregardlessofpotentialtypeargumentnullability.NoticethatwerestoredthetypeparameterTboundasnon-nullabletypeAction,becausewewanttostorenon-nullabletypesanddealwithnullabilityonlyforcertainscenarios(suchasanon-existinglastelement).Let'suseourupdatedActionGroupclass:
valactionGroup=ActionGroup<Action>(listOf())
valactionGroup=actionGroup.lastOrNull()
//InferredtypeisAction?
println(actionGroup?.name)//Prints:null
NoticethattheactionGroupinferredtypeisnullableevenifweparameterizedthegenericwithanon-nullabletypeargument.
Anullabletypeattheuse-sitedoesnotstopusfromallowingnon-nulltypesinthedeclaration-site:
openclassAction
classActionGroup<T:Action?>(privatevallist:List<T>){
funlastOrNull():T?=list.lastOrNull()
}
//Usage
valactionGroup=ActionGroup(listOf(Action(),null))
println(actionGroup.lastOrNull())//Prints:null
Let'ssumuptheabovesolution.Wespecifiedanon-nullableboundfortypeparametertodisallowparameterizingtheActionGroupclasswithnullabletypesastypearguments.WeparameterizedtheActionGroupclasswiththenon-nullabletypeargumentAction.Finally,weenforcedtypeparameternullabilityattheuse-site(T?),becausethelastpropertycanreturnnulliftherearenoelementsinthelist.
VarianceSubtypingisapopularconceptintheOOPparadigm.Wedefineinheritancebetweentwoclassesbyextendingtheclass:
openclassAnimal(valname:String)
classDog(name:String):Animal(name)
TheclassDogextendstheclassAnimal,sothetypeDogisasubtypeofAnimal.ThismeansthatwecanuseanexpressionoftypeDogwheneveranexpressionoftypeAnimalisrequired;forexample,wecanuseitasafunctionargumentorassignavariableoftypeDogtoavariableoftypeAnimal:
funpresent(animal:Animal){
println("Thisis${animal.name}")
}
present(Dog("Pluto"))//Prints:ThisisPluto
Beforewemoveon,weneedtodiscussthedifferencebetweenclassandtype.Typeisamoregeneralterm--itcanbedefinedbyclassorinterface,oritcanbebuiltintothelanguage(primitivetype).InKotlin,foreachclass(forexample,Dog),wehaveatleasttwopossibletypes--non-nullable(Dog)andnullable(Dog?).Whatismore,foreachgenericclass(forexample,classBox<T>)wecandefinemultipledatatypes(Box<Dog>,Box<Dog?>,Box<Animal>,Box<Box<Dog>>,andsoon).
Thepreviousexampleappliesonlytosimpletypes.Variancespecifieshowsubtypingbetweenmorecomplextypes(forexample,Box<Dog>andBox<Animal>)relatestosubtypingbetweentheircomponents(forexample,Animal,andDog).
InKotlin,genericsareinvariantbydefault.ThismeansthatthereisnosubtypingrelationbetweenthegenerictypesBox<Dog>andBox<Animal>.TheDogcomponentissubtypeofAnimal,butBox<Dog>isneitherasubtypenorasupertypeofBox<Animal>:
classBox<T>
openclassAnimal
classDog:Animal()
varanimalBox=Box<Animal>()
vardogBox=Box<Dog>()
//oneofthelinesbelowlinemustbecommentedout,
//otherwiseAndroidStudiowillshowonlyoneerror
animalBox=dogBox//2,error
dogBox=animalBox//1,error
1. ErrorTypemismatch.RequiredBox<Animal>,foundBox<Dog>.2. ErrorTypemismatch.RequiredBox<Dog>,foundBox<Animal>.
TheBox<Dog>typeisneitherasubtypenorasupertypeofBox<Animal>,sowecan'tuseanyoftheassignmentsshownintheprecedingcode.
WecandefinesubtypingrelationsbetweenBox<Dog>andBox<Animal>.InKotlin,asubtypingrelationofgenerictypecanbepreserved(co-variant),reversed(contra-variant),orignored(invariant).
Whenasubtypingrelationisco-variant,itmeansthatsubtypingispreserved.Thegenerictypewillhavethesamerelationasthetypearguments.IfDogisasubtypeofAnimal,thenBox<Dog>isasubtypeofBox<Animal>.
Contra-variantistheexactoppositeofco-variant,wheresubtypingisreversed.Thegenerictypewillhavereversedrelationwithrespecttotypearguments.IfDogisasubtypeofAnimal,thenBox<Animal>isasubtypeofBox<Dog>.Thefollowingdiagrampresentalltypesofvariance:
Todefineco-variantorcontra-variantbehavior,weneedtousevariancemodifiers.
VariancemodifiersGenericsinKotlinareinvariantbydefault.Thismeansthatweneedtousetypeasthetypeofdeclaredvariableorfunctionparameter:
publicclassBox<T>{}
funsum(list:Box<Number>){/*...*/}
//Usage
sum(Box<Any>())//Error
sum(Box<Number>())//Ok
sum(Box<Int>())//Error
Wecan'tuseagenerictypeparametrizedwithInt,whichisasubtypeofNumber,andAny,whichisasupertypeofNumber.Wecanrelaxthisrestrictionandchangethedefaultvariancebyusingvariancemodifiers.InJava,thereisquestionmark(?)notation(wildcardnotation)usedtorepresentanunknowntype.Usingit,wecandefinetwotypesofwildcardbounds--upperboundandlowerbound.InKotlin,wecanachievesimilarbehaviorusinginandoutmodifiers.
InJava,theupperboundwildcardallowsustodefineafunctionthatacceptsanyargumentthatisacertaintypeofitssubtype.Inthefollowingexample,thesumfunctionwillacceptanyListthatwasparametrizedwiththeNumberclassorasubtypeoftheNumberclass(Box<Integer>,Box<Double>,andsoon):
//Java
publicvoidsum(Box<?extendsNumber>list){/*...*/}
//Usage
sum(newBox<Any>())//Error
sum(newBox<Number>())//Ok
sum(newBox<Int>())//Ok
WecannowpassBox<Number>tooursumfunctionandallthesubtypes,forexample,Box<Int>.ThisJavabehaviorcorrespondstotheKotlinoutmodifier.Itrepresentscovariance,whichrestrictsthetypetobeaspecifictypeorasubtypeofthattype.ThismeansthatwecansafelypassinstancesoftheBoxclassthatareparametrizedwithanydirectorindirectsubclassofNumber:
classBox<T>
funsum(list:Box<outNumber>){/*...*/}
//usage
sum(Box<Any>())//Error
sum(Box<Number>())//Ok
sum(Box<Int>())//Ok
InJava,thelowerboundwildcardallowsustodefineafunctionthatacceptsanyargumentthatisacertaintypeoritssupertype.Inthefollowingexample,thesumfunctionwillacceptanyListthatwasparametrizedwiththeNumberclassorasupertypeoftheNumberclass(Box<Number>andBox<Object>):
//Java
publicvoidsum(Box<?superNumber>list){/*...*/}
//usage
sum(newBox<Any>())//Ok
sum(newBox<Number>())//Ok
sum(newBox<Int>())//Error
WecannowpassBox<Any>tooursumfunctionandallthesubtypes,forexample,Box<Any>.ThisJavabehaviorcorrespondstotheKotlininmodifier.Itrepresentscontra-variance,whichrestrictsthetypetobeaspecifictypeorasupertypeofthattype:
classBox<T>
funsum(list:Box<inNumber>){/*...*/}
//usage
sum(Box<Any>())//Ok
sum(Box<Number>())//Ok
sum(Box<Int>())//Error
It'sforbiddentouseaninandoutmodifiertogether.Wecandefinevariancemodifiersintwodifferentways.Let'slookatthemintheupcomingsection.
Use-sitevarianceversusdeclaration-sitevarianceUse-sitevarianceanddeclaration-sitevariancebasicallydescribestheplaceinthecode(site)wherethevariancemodifierisspecified.Let'sconsidertheViewandPresenterexample:
interfaceBaseView
interfaceProductView:BaseView
classPresenter<T>
//Usage
varpreseter=Presenter<BaseView>()
varproductPresenter=Presenter<ProductView>()
preseter=productPresenter
//Error:Typemismatch
//Required:Presenter<BaseView>
//Found:Presenter<ProductView>
TheclassPresenterisinvariantonitstypeparameterT.Tofixtheproblem,wecanexplicitlydefinethesubtypingrelation.Wecandoitintwoways(use-siteanddeclaration-site).First,let'sdefinethevarianceattheuse-site:
varpreseter:Presenter<outBaseView>=Presenter<BaseView>()//1
varproductPresenter=Presenter<ProductView>()
preseter=productPresenter
1. Variancemodifierdefinedattypeargumentuse-site
NowthepresetervariablecanstoresubtypesofPresenter<BaseView>,includingPresenter<ProductView>.Oursolutionworks,butourimplementationcanbeimproved.Therearetwoproblemswiththisapproach.Nowweneedtospecifythisoutvariancemodifiereachtimewewanttouseagenerictype,forexample,useitinmultiplevariablesacrossdifferentclasses:
//VariabledeclaredinsideclassAandclassB
varpreseter=Presenter<BaseView>()
varpreseter:Presenter<outBaseView>=Presenter<ProductView>()
preseter=productPresenter
BothclassesAandBcontainsthepresetervariablethathasvariancemodifier.We
losetheabilitytousetypeinferenceandinresultthecodeismoreverbose.Toimproveourcodewecanspecifyvariancemodifierattypeparameterdeclaration-site:
interfaceBaseView
interfaceProductView:BaseView
classPresenter<outT>//1
//usage
//VariabledeclaredinsideclassAandB
varpreseter=Presenter<BaseView>()
varproductPresenter=Presenter<ProductView>()
preseter=productPresenter
1. Variancemodifierdefinedattypeparameterdeclaration-site
WeonlyneedtodefinevariancemodifieronceinsidePresenterclass.Infact,bothprecedingimplementationsaretheequivalent,althoughdeclaration-sitevarianceismoreconciseanditcanbeeasierusedbyexternalclientsoftheclass
CollectionvarianceInJava,arraysareco-variant.Bydefault,wecanpassanarrayofString[]evenifanarrayofObject[]isexpected:
publicclassComputer{
publicComputer(){
String[]stringArray=newString[]{"a","b","c"};
printArray(stringArray);//PassinstanceofString[]
}
voidprintArray(Object[]array){
//DefineparameteroftypeObject[]
System.out.print(array);
}
}
ThisbehaviorwasimportantinearlyversionsofJava,becauseitallowedustousedifferenttypesofarraysasarguments:
//Java
staticvoidprint(Object[]array){
for(inti=0;i<=array.length-1;i++)
System.out.print(array[i]+"");
System.out.println();
}
//Usage
String[]fruits=newString[]{"Pineapple","Apple","Orange",
"Banana"};
print(fruits);//Prints:PineappleAppleOrangeBanana
Arrays.sort(fruits);
print(fruits);//Prints:AppleBananaOrangePineapple
Butthisbehavioralsomayleadtopotentialruntimeerrors:
publicclassComputer{
publicComputer(){
Number[]numberArray=newNumber[]{1,2,3};
updateArray(numberArray);
}
voidupdateArray(Object[]array){
array[0]="abc";
//Error,java.lang.ArrayStoreException:java.lang.String
}
}
ThefunctionupdateArrayacceptsparametersoftypeObject[]andwearepassingString[].WearecallingtheaddmethodwithaStringparameter.WecandosobecausearrayitemsareoftypeObject,sowecanuseString,whichisanewvalue.
Finally,wewanttoaddString,intothegenericarraythatmayonlycontainitemsofStringtype.Duetodefaultco-variantbehavior,thecompilercan'tdetectthisproblemandthiswillleadtoanArrayStoreExceptionexception.
ThecorrespondingcodewouldnotcompileinKotlin,becausetheKotlincompilertreatsthisbehavioraspotentiallydangerous.ThisisthereasonwhyarraysinKotlinareinvariant.Therefore,passingtypeotherthanArray<Number>whenArray<Any>isrequiredwillresultincompiletimeerror:
publicclassArray<T>{/*...*/}
Therefore,passingatypeotherthanArray<Number>whenArray<Any>isrequiredwillresultinacompile-timeerror:
publicclassArray<T>{/*...*/}
classComputer{
init{
valnumberArray=arrayOf<Number>(1,2,3)
updateArray(numberArray)
}
internalfunupdateArray(array:Array<Any>){
array[0]="abc"
//error,java.lang.ArrayStoreException:java.lang.String
}
}
Noticethatapotentialruntimeexceptionmayonlyoccurwhenwecanmodifytheobject.VarianceisalsoappliedtoKotlincollectioninterfaces.IntheKotlinstandardlibrary,wehavetwolistinterfacesthataredefinedindifferentways.TheKotlinListinterfaceisdefinedasco-variant,becauseitisimmutable(itdoesnotcontainanymethodsthatwouldallowustochangetheinnerstate),whiletheKotlinMutableListinterfaceisinvariant.Herearethedefinitionsoftheirtypeparameters:
interfaceList<outE>:Collection<E>{/*...*/}
publicinterfaceMutableList<E>:List<E>,MutableCollection<E>{
/*...*/
}
Let'sseetheconsequencesofsuchdefinitionsinaction.Itmakesmutablelistssafefromtherisksofcovariance:
funaddElement(mutableList:MutableList<Any>){
mutableList.add("Cat")
}
//Usage
valmutableIntList=mutableListOf(1,2,3,4)
valmutableAnyList=mutableListOf<Any>(1,'A')
addElement(mutableIntList)//Error:Typemismatch
addElement(mutableAnyList)
Thelistissafe,becauseithasnomethodsusedtochangeitsinnerstate,anditscovariancebehaviorallowsmoregeneralusageoffunctions:
funprintElements(list:List<Any>){
for(einlist)print(e)
}
//Usage
valintList=listOf(1,2,3,4)
valanyList=listOf<Any>(1,'A')
printElements(intList)//Prints:1234
printElements(anyList)//Prints:1A
WecanpassList<Any>oranyofitssubtypestotheprintElementsfunction,becausetheListinterfaceisco-variant.WecanonlypassMutableList<Any>totheaddElementfunctionbecausetheMutableListinterfaceisinvariant.
Usinginandoutmodifiers,wecanmanipulatevariancebehavior.Weshouldalsobeawarethatvariancehassomelimitations.Let'sdiscussthem.
Varianceproducer/consumerlimitationByapplyingavariancemodifier,wegainco-variant/contra-variantbehaviorforacertaintypeparameteroftheclass/interface(declaration-sitevariance)ortypeargument(use-sitevariance).However,thereisalimitationthatweneedtobeawareof.Tomakeitsafe,theKotlincompilerlimitsthepositionswheretypeparameterscanbeused.
Withinvariant(defaultnovariancemodifierontypeparameter)wecanuseatypeparameteronbothin(typeoffunctionparameter)andout(functionreturntype)positions:
interfaceStack<T>{
funpush(t:T)//Generictypeatinposition
funpop():T//Generictypeatoutposition
funswap(t:T):T//Generictypeatinandoutpositions
vallast:T//Generictypeatoutposition
varspecial:T//Generictypeatoutposition
}
Withvariancemodifier,weareonlylimitedtoasingleposition.Thismeansthatwecanuseatypeparameteronlyasatypeformethodparameters(in)ormethodreturnvalue(out).Ourclasscanbeproducerorconsumer,butneverboth.Wecansaythattheclasstakesinparametersorgivesoutparameters.
Let'slookathowthisrestrictionrelatestovariancemodifiersspecifiedatthedeclaration-site.HereareallthecorrectandincorrectusagesforthetwotypeparametersRandT:
classConsumerProducer<inT,outR>{
funconsumeItemT(t:T):Unit{}//1
funconsumeItemR(r:R):Unit{}//2,error
funproduceItemT():T{//3,error
//ReturninstanceoftypeT
}
funproduceItemR():R{//4
//ReturninstanceoftypeR
}
}
1. OKtypeparameterTatinposition2. Error,typeparameterRatinposition3. Error,typeparameterTatoutposition4. OK,typeparameterRatoutposition
Aswecansee,thecompilerwillreportanerroriftheconfigurationisprohibited.NoticethatwecanadddifferentmodifiersforthetwotypeparametersRandT.
Positionrestrictionappliesonlyformethodsaccessible(visible)outsidetheclass.Thismeansnotonlyallpublicmethods(publicisthedefaultmodifier)asusedpreviously,butalsomethodsmarkedwithprotectedorinternal.Whenwechangemethodvisibilitytoprivate,thenwecanuseourtypeparameters(RandT)onanyposition,justlikeinvarianttypeparameters:
classConsumerProducer<inT,outR>{
privatefunconsumeItemT(t:T):Unit{}
privatefunconsumeItemR(r:R):Unit{}
privatefunproduceItemT():T{
//ReturninstanceoftypeT
}
privatefunproduceItemR():R{
//ReturninstanceoftypeR
}
}
Let'slookatthefollowingtable,whichpresentsallallowedpositionsfortypeparametersusedastype:
Visibilitymodifier Invariance Covariance(out) Contravariance(in)
public,protected,internal in/out out in
private in/out in/out in/out
InvariantconstructorThereisoneimportantexceptionfortheinandoutpositionrulesdescribedintheprevioussection:constructorparametersarealwaysinvariant:
classProducer<outT>(t:T)
//Usage
valstringProducer=Producer("A")
valanyProducer:Producer<Any>=stringProducer
Theconstructorispublic,typeparameterTisdeclaredasout,butwecanstilluseitasconstructorparametertypeattheinposition.Thereasonisthattheconstructormethodcan'tbecalledafteraninstanceiscreated,soitisalwayssafetocallit.
AswediscussedinChapter4,ClassesandObjects,wecanalsodefineapropertydirectlyintheclassconstructorusingavalorvarmodifier.Whencovarianceisspecified,wecanonlydefinearead-onlyproperty(val)intheconstructorthathasco-varianttype.Itissafebecauseonlythegetterwillbegenerated,sothevalueofthispropertycan'tchangeafterclassinstantiation:
classProducer<outT>(valt:T)//Ok,safe
Withvar,bothgetterandsetteraregeneratedbythecompiler,sothepropertyvaluecanpotentiallychangeatsomepoint.That'swhywecan'tdeclarearead-write(var)propertyofco-varianttypeintheconstructor:
classProducer<outT>(vart:T)//Error,notsafe
Wealreadysaidthatvariancerestrictiononlyappliesforexternalclients,sowecouldstilldefineaco-variantread-writepropertybyaddingaprivatevisibilitymodifier:
classProducer<outT>(privatevart:T)
Anotherpopulargenerictyperestriction,knownfromJava,relatestotypeerasure.
TypeerasureTypeerasurewasintroducedintoJVMtomakeJVMbytecodebackwardcompatiblewithversionsthatpredatetheintroductionofgenerics.OntheAndroidplatform,bothKotlinandJavaarecompiledtoJVMbytecode,sotheybotharevulnerabletotypeerasure.
Typeerasureistheprocessofremovingatypeargumentfromagenerictype,sothatthegenerictypelosessomeofitstypeinformation(typeargument)atruntime:
packagetest
classBox<T>
valintBox=Box<Int>()
valstringBox=Box<String>()
println(intBox.javaClass)//prints:test.Box
println(stringBox.javaClass)//prints:test.Box
Thecompilercandistinguishbothtypesandguaranteetypesafety.However,duringcompilation,theparameterizedtypesBox<Int>andBox<String>aretranslatedbythecompilertoaBox(rawtype).ThegeneratedJavabytecodedoesnotcontainanyinformationrelatedtotypearguments,sowecan'tdistinguishgenerictypesatruntime.
Typeerasureleadstoafewproblems.InJVM,wecan'tdeclaretwooverloadsofthesamemethod,withthesameJVMsignature:
/*
java.lang.ClassFormatError:Duplicatemethodname&signature...
*/
funsum(ints:List<Int>){
println("Ints")
}
funsum(strings:List<String>){
println("Ints")
}
Whenthetypeargumentisremoved,thosetwomethodswillhaveexactlythesamedeclaration:
/*
java.lang.ClassFormatError:Duplicatemethodname&signature...
*/
funsum(ints:List){
println("Ints")
}
funsum(strings:List){
println("Ints")
}
WecanalsosolvethisproblembychangingtheJVMnameofthegeneratedfunction.WecandoitusingJvmNameannotationtochangethenameofoneofthemethodswhenthecodeiscompiledtoJVMbytecode:
@JvmName("intSum")funsum(ints:List<Int>){
println("Ints")
}
funsum(strings:List<String>){
println("Ints")
}
NothingchangedinthisfunctionusagefromKotlin,butsincewechangedtheJVMnameofthefirstfunction,weneedtouseanewnametouseitfromJava:
//Java
TestKt.intSum(listOfInts);
Sometimeswewanttopreservethetypeargumentatruntimeandthisiswherereifiedtypeparametersarequitehandy.
ReifiedtypeparametersTherearesomecaseswhereaccessingthetypeparameteratruntimewouldbeuseful,buttheyarenotallowedbecauseoftypeerasure:
fun<T>typeCheck(s:Any){
if(sisT){
//Error:cannotcheckforinstanceoferasedtype:T
println("Thesametypes")
}else{
println("Differenttypes")
}
}
TobeabletoovercomeJVMlimitation,Kotlinallowsustouseaspecialmodifierthatcanpreserveatypeargumentatruntime.Weneedtomarkthetypeparameterwiththereifiedmodifier:
interfaceView
classProfileView:View
classHomeView:View
inlinefun<reifiedT>typeCheck(s:Any){//1
if(sisT){
println("Thesametypes")
}else{
println("Differenttypes")
}
}
//Usage
typeCheck<ProfileView>(ProfileView())//Prints:Thesametypes
typeCheck<HomeView>(ProfileView())//Prints:Differenttypes
typeCheck<View>(ProfileView())//Prints:Thesametypes
1. Typeparametermarkedasrefinedandfunctionmarkedasinline.
Nowwecansafelyaccessthetypeargumenttypeatruntime.Reifiedtypeparametersworkonlywithinlinefunctions,becauseduringcompilation(inlining),theKotlincompilerreplacesreifiedtypeargumentactualclass.Thisway,thetypeargumentwillnotberemovedbytypeerasure.
Wecanalsousereflectiononareifiedtypetoretrievemoreinformationaboutthetype:
inlinefun<reifiedT>isOpen():Boolean{
returnT::class.isOpen
}
OccurrencesofareifiedtypeparameterarerepresentedatJVMbytecodelevelasanactualtypeorawrappertypeforprimitivetypes.That'swhyreifiedtypeparametersarenotaffectedbytypeerasure.
Usingreifiedtypeparametersallowsustowritemethodsinawholenewway.TostartanewActivityinJava,weneedcodelikethis:
//Java
startActivity(Intent(this,ProductActivity::class.java))
InKotlin,wecandefinethestartActivitymethodthatwillallowustonavigatetoActivityinmuchsimplerway:
inlinefun<reifiedT:Activity>startActivity(context:Context){
context.startActivity(Intent(context,T::class.java))
}
//Usage
startActivity<UserDetailsActivity>(context)
WedefinedthestartActivitymethodandwepassedinformationabouttheActivitywewanttostart(ProductActivity)byusingatypeargument.WealsodefinedanexplicitreifiedtypeparameterboundtomakesurethatwecanonlyuseActivity(anditssubclasses)astypeargument.
ThestartActivitymethodTomakeproperuseofthestartActivitymethod,weneedawaytopassparameterstotheActivitybeingstarted(Bundle).Itispossibletoupdatetheprecedingimplementationtosupportargumentslikethis:
startActivity<ProductActivity>("id"to123,"extended"totrue)
Intheprecedingexample,argumentsarefilledusingakeyandvalueprovidedbypairs(definedbytheinlinetofunction).Thisfunctionimplementationis,however,outsideofthescopeofthisbook.Wecan,however,useanexistingone.TheAnkolibrary(https://github.com/Kotlin/anko)alreadyimplementsthestartActivitymethodwithalltherequiredfunctionality.WejustneedtoimportAppcompat-v7-commonsdependency.
compile"org.jetbrains.anko:anko-appcompat-v7-commons:$anko_version"
AnkodefinesextensionsforContextandFragmentclassessowecanusethismethodinanyActivityorFragmentjustlikeanyothermethoddefinedintheclasswithouttheneedtodefinethemethodintheclass.WewilldiscussextensionsinChapter7,ExtensionFunctionsandProperties.
Beawarethatreifiedtypeparametershaveonemainlimitation:wecan'tcreateaninstanceofaclassfromareifiedtypeparameter(withoutreflectionusage).Thereasonbehindthisisthataconstructorisalwaysonlyassociatedtoaconcreteinstance(itisneverinherited)sothereisnoconstructorthatcouldbesafelyusedforallpossibletypeparameters.
Star-projectionsBecauseoftypeerasure,incompletetypeinformationisavailableatruntime.Forexample,typeparametersofgenerictypesarenotavailable:
vallist=listOf(1,2,3)
println(list.javaClass)//Prints:classjava.util.Arrays$ArrayList
Thisleadstoafewproblems.Wecan'tperformanychecktoverifywhattypesofelementsListcontains:
/*
Compiletimeerror:cannotcheckinstanceoferasedtype:
List<String>
*/
if(collectionisList<Int>){
//...
}
Theproblemoccursbecauseacheckisperformedatruntimewhereinformationabouttypeparametersisnotavailable.Kotlin,however,asopposedtoJava,doesnotallowustodeclarerawtype(generictypethatisnotparametrizedwithtypeargument):
SimpleList<>//Java:ok
SimpleList<>//Kotlin:error
Kotlinallowsustousestar-projectionsyntaxinstead,whichisbasicallyawaytosaythatinformationabouttypeargumentismissingoritisnotimportant:
if(collectionisList<*>){
//...
}
Byusingstar-projectionsyntax,wesaythatBoxstoresargumentsofacertaintype:
classBox<T>
valanyBox=Box<Any>()
valintBox=Box<Int>()
valstringBox=Box<String>()
varunknownBox:Box<*>
unknownBox=anyBox//Ok
unknownBox=intBox//Ok
unknownBox=stringBox//Ok
NoticethatthereisadifferencebetweenBox<*>andBox<Any>.IfwewanttodefinelistcontainsitemsofAnywewoulduseBox<Any>.Howeverifwewanttodefinelistthatcontainstermsofcertaintype,butthistypeisunknown(itmaybeAny,Int,String,andsoon.Butwedon’thaveinformationaboutthistype),whileBox<Any>meansthatlistcontainsitemsofAnytype.WewilluseBox<*>:
valanyBox:Box<Any>=Box<Int>//Error:Typemismatch
Ifagenerictypedefineswithmultipletypeparameters,weneedtouseastar(*)foreachmissingtypeargument:
classContainer<T,T2>
valcontainer:Container<*,*>
Star-projectionisalsohelpfulwhenwewanttoperformanoperationonthetype,butinformationabouttypeargumentisnotimportant:
funprintSize(list:MutableList<*>){
println(list.size)
}
//usage
valstringList=mutableListOf("5","a","2","d")
valintList=mutableListOf(3,7)
printSize(stringList)//prints:4
printSize(intList)//prints:2
Intheprecedingexample,theinformationabouttypeargumentisnotrequiredtodeterminecollectionsize.Usingstar-projectionsyntaxreducestheneedforvariancemodifiersaslongaswedon'tuseanymethodsthatdependonatypeargument.
TypeparameternamingconventionsTheofficialJavatypeparameternamingconvention(https://docs.oracle.com/javase/tutorial/java/generics/types.html)definesthefollowingguidelinesforparameternaming:
Byconvention,typeparameternamesaresingle,uppercaseletters.Thisstandsinsharpcontrasttothevariablenamingconventionsthatyoualreadyknowabout,andwithgoodreason.Withoutthisconvention,itwouldbedifficulttotellthedifferencebetweenatypevariableandanordinaryclassorinterfacename.Themostcommonlyusedtypeparameternamesare:
E:Element(usedextensivelybytheJavaCollectionsFramework)K:KeyN:NumberT:TypeV:ValueS,U,V,andsoon:2nd,3rd,4thtypes
ManyclassesintheKotlinstandardlibraryfollowthisconvention.Itworksfineforpopularkindsofclassessuchascommonclasses(List,Mat,Set,andsoon)orclassesthatdefineasimpletypeparameter(Box<T>class).However,withcustomclassesandmultipletypeparameters,wequicklyrealizethatasingleletterdoesnotcontainasufficientamountofinformationandsometimesit'shardtoquicklytellwhatkindofdatathetypeparameterrepresents.Thereareafewsolutionsforthisproblem.
Wecouldmakesurethatgenericsareproperlydocumentedand,yes,thiswoulddefinitelyhelp,butwestillwouldn'tbeabletodeterminethemeaningofatypeparameterjustbylookingatthecode.Documentationisimportant,butweshouldtreatdocumentationasanauxiliarysourceofinformationandstriveforthehighestpossiblecodereadability.
Overtheyears,programmershavestartedtomigrateintomoremeaningfulnamingconventions.TheGoogleJavaStyleGuide(https://google.github.io/styleguide/javaguide.html#s5.2.8-type-variable-names)brieflydescribesamixoftheofficialJavatypeparameternamingconventionandcustomnamingconventions.Theypromote
twodistinctstyles.Thefirstistouseasinglecapitalletter,optionallyfollowedbyasinglenumeral(asopposedtotheS,U,VnamesdescribedbyJava):
classBox<T,T2>
Thesecondstyleismoredescriptivebecauseitaddsameaningfulprefixfortypeparameter:
classBox<RequestT>
Unfortunately,thereisnosinglestandardfortypeparameternames.Themostcommonsolutionistheuseofasingleuppercaseletter.Thosearesimplifiedexamples,butkeepinmindthatclassesusuallyusegenericsinmultipleplaces,sopropernamingwillimproveyourcodereadability.
SummaryInthischapter,wehavelearnedwhygenericsexistandwehavediscussedvariouswaysofdefiningagenericclassandinterface,anddeclaringgenerictypes.Weknowhowtodealwithsubtypingrelationsbyusinguse-siteanddeclaration-sitevariancemodifiers.Welearnedhowtodealwithtypeerasureandhowtopreservegenerictypeatruntimeusingreifiedtypeparameters.
Inthenextchapter,wewilldiscussoneofthemostexcitingKotlinfeatures-extensions.Thisfeatureallowsustoaddnewbehaviortoanexistingclass.Wewilllearnhowwecanimplementnewmethodsandpropertiesforanygivenclass,includingfinalclassesfromtheAndroidframeworkandthird-partylibraries.
ExtensionFunctionsandPropertiesInpreviouschapters,mostoftheconceptswerefamiliartoJavadevelopers.Inthischapter,weareintroducingafeaturethatwasnotknowninJavaatall--extensions.ItisoneofthebestKotlinfeatures,andlotsofKotlindevelopersarementioningitastheirfavoriteone.ExtensionsaremakingabigimprovementinAndroiddevelopment.
Inthischapter,wewillcoverthefollowingtopics:
ExtensionfunctionsExtensionpropertiesMemberextensionfunctionsGenericextensionfunctionsCollectionprocessingFunctiontypewithreceiverandfunctionliteralwithreceiverKotlingenericextensionfunctionstoanyobjectKotlindomain-specificlanguage
ExtensionfunctionsAllbiggerJavaprojectshaveutilityclasses,suchasStringUtils,ListUtils,AndroidUtils,andsoon.Itissopopularbecauseutilfunctionscapturecommonpatternsandallowthemtobetestedandusedinasimplerway.TheproblemwasthatJavareallypoorlysupportsthecreationandusageofsuchfunctions,becausetheyhavetobeimplementedasstaticfunctionsofsomeclass.Let'sdiscussthisproblemwithanexample.EveryJavaAndroiddeveloperknowswellthefollowingcodeusedtoshowToast:
Toast.makeText(context,text,Toast.LENGTH_SHORT).show();
ItiscommonlyusedinAndroidprojectsforshowingerrorsorshortmessages,andoftenitispresentedatthebeginningofmostAndroidtutorials.Codethatimplementsthisfunctionalityisverbose,becauseofhowitisusingastaticfunctionthatisusedlikeabuilder.ProbablyeveryJavaAndroiddeveloperatleastoncehasforgottentoinvoketheshowmethodonareturnedobject,whichmadehimcheckallsurroundingconditionstofindoutwhythisisnotworking.Thisallmakesthissimplefunctionalityaperfectcandidatetobepackedasanutilfunction.Butitisreallyrarelyusedthisway.Why?Tounderstandit,let'sfirstlookathowitcouldbeimplementedinJava:
publicclassAndroidUtils{
publicstaticvoidtoast(Contextcontext,Stringtext){
Toast.makeText(context,text,Toast.LENGTH_SHORT).show();
}
}
//Usage
AndroidUtils.toast(context,"Sometoast");
Whenaprogrammerwantstousethefollowingfunction,theyneedstorememberthatthereissuchfunction,inwhichclassitislocalized,andwhatitsnameis.Therefore,itsusageisnotsimplerthanprevious.ItisimpossibletoimplementitasamethodofContext(asuperclassofActivity)withoutchangingtheAndroidSDKimplementation,butinKotlin,itispossibletocreateanextensionfunction,whichactssimilarlytoanactualmethoddefinedinsideaclass.HereishowwecanimplementtoastasanextensiontoContext:
funContext.toast(text:String){//1
Toast.makeText(this,text,LENGTH_LONG).show()//2
}
//Usage
context.toast("Sometoast")
1. Contextisnotontheargumentlist,butbeforethefunctionname.Thisishowwedefinewhattypeweareextending.
2. Insidethefunctionbody,wecanusethethiskeywordtoreferencetheobjectonwhichtheextensionfunctionisinvoked.
Theonlydifferenceinthegeneralstructurebetweenanextensionfunctionandastandardfunctionisthatthereisareceivertypespecifiedbeforethefunctionname.Alessvisiblechangeisinsidethebody--there,wecanaccessthereceiverobject(theobjectonwhichanextensioniscalled)bythethiskeyword,ordirectlycallitsfunctionsorproperties.Withsuchadefinition,thetoastfunctionactslikeamethoddefinedinContext:
context.toast("Sometoast")
Alternatively:
classMainActivity:Activity(){
overridefunonCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
toast("Sometext")
}
}
Thismakesusageofthetoastfunctionmucheasierthanimplementationofthewholetoast-displayingcode.WealsogetsuggestionsfromtheIDE,thatwecaninvokethisfunctionwhenweareinsideContext(likeinsideActivity)oronaninstanceofContext:
Intheprecedingexample,Contextisareceivertypeofthetoastfunction,andthethisinstanceisareferencetothereceiverobject.Allfunctionsandpropertiesofthereceiverobjectcanbeaccessedexplicitly,sowecantakethefollowingdefinition:
funCollection<Int>.dropPercent(percent:Double)
=this.drop(floor(this.size*percent)
Wecanthenreplaceitwiththefollowing:
funCollection<Int>.dropPercent(percent:Double)
=drop(floor(size*percent))
Therearemultipleusecaseswhereextensionfunctionsareuseful.SimilarextensionfunctionscanbedefinedforView,List,String,andotherclassesdefinedintheAndroidframeworkorathird-partylibraryandcustomclassesdefinedbythedeveloper.Extensionfunctionscanbeaddedtoanyaccessibletype,eventotheAnyobject.Hereisanextensionfunctionthatcanbecalledoneveryobject:
funAny?.logError(error:Throwable,message:String="error"){
Log.e(this?.javaClass?.simpleName?:"null",message,error)
}
Herearesomecallexamples:
user.logError(e,"NameError")//Logs:User:NameError...
"String".logError(e)//String:error...
logError(e)//1,MainActivity:error...
1. SupposingthatweareinvokingthisinMainActivity.
Wecansimplyaddanymethodtoanyclasswewant.ThisisagreatimprovementforAndroiddevelopment.Withit,wehaveawaytoaddmissingmethodsorpropertiestotypes.
ExtensionfunctionsunderthehoodWhileKotlinextensionfunctionsmightlookmagical,theyarereallysimpleunderthehood.Atop-levelextensionfunctioniscompiledtoastaticfunctionwithareceiverobjectonthefirstargument.Let'slookatthealreadypresentedtoastfunction:
//ContextExt.kt
funContext.toast(text:String){
Toast.makeText(this,text,LENGTH_LONG).show()
}
Thisfunction,aftercompilationanddecompilationtoJava,wouldlooksimilartothefollowingfunction:
//Java
publicclassContextExtKt{
publicstaticvoidtoast(Contextreceiver,Stringtext){
Toast.makeText(receiver,text,Toast.LENGTH_SHORT).show();
}
}
Kotlintop-levelextensionfunctionsarecompiledtostaticfunctionswithareceiverobjectonthefirstparameter.ThisiswhywecanstilluseextensionsfromJava:
//Java
ContextExtKt.toast(context,"Sometoast")
Also,thismeansthatfromaJVMbytecodeperspective,themethodisnotreallyadded,butduringcompilationallextensionfunctionusagesarecompiledtostaticfunctioncalls.Whileextensionfunctionsarejustfunctions,functionmodifierscanbeappliedtothemthesameastheycanbealsoappliedtoanyotherfunction.Forexample,anextensionfunctioncanbemarkedasinline:
inlinefunContext.isPermissionGranted(permission:String):Boolean=ContextCompat.checkSelfPermission(this,permission)==PackageManager.PERMISSION_GRANTED
Aswithotherinlinefunctions,thefunctioncallwillbereplacedwithanactualbodyduringapplicationcompilation.Wecandowithextensionfunctionspracticallyeverythingwecandowithotherfunctions.Theycanbesingleexpression,havedefaultarguments,beusedbynamedparameters,andsoon.
Buttherearealsoother,lessintuitiveconsequencesofsuchimplementation.Inthenextsections,wearegoingtodescribethem.
NomethodoverridingWhenthereisamemberfunctionandanextensionfunctionwiththesamenameandparameters,thememberfunctionalwayswins.Hereisanexample:
classA{
funfoo(){
println("foofromA")
}
}
funA.foo(){
println("foofromExtension")
}
A().foo()//Prints:foofromA
Thisisalwaystrue.Evenmethodsfromasuperclasswinwithextensionfunctions:
openclassA{
funfoo(){
println("foofromA")
}
}
classB:A()
funB.foo(){
println("foofromExtension")
}
A().foo()//foofromA
Thepointisthattheextensionfunctionisnotallowingtomodifythebehaviorofarealobject.Wecanonlyaddextrafunctionalities.Thiskeepsussecured,becauseweknowthatnoonewillchangethebehaviorofobjectsthatweareusing,whichmightleadtoerrorsthatarehardtotrack.
AccesstoreceiverelementsAnextensionfunctioniscompiledtoastaticfunctionwithareceiverobjectonthefirstparameter,sowehavenoextraaccessprivilege.Theprivateandprotectedelementsarenotaccessible,andelementswithJavadefault,Javapackage,orKotlininternalmodifiersareaccessedthesameasifwewouldjustoperateonstandardobject.
Thankstothat,theseelementsareprotectedastheyshouldbe.Rememberthatextensionfunctions,whilebeingreallypowerfulanduseful,arejustsyntacticsugar,andthereisnomagicthere.
ExtensionsareresolvedstaticallyExtensionfunctionsarejustfunctionswithareceiverasthefirstparameter,sotheircallsareresolvedatcompiletimebythetypeonwhichthefunctionisinvoked.Forexample,whenthereareextensionfunctionsforbothsuperclassandsubclass,thentheextensionfunctionsthatwillbechosenduringinvocationdependonthetypeofpropertyonwhichweareoperating.Hereisanexample:
abstractclassA
classB:A()
funA.foo(){println("foo(A)")}
funB.foo(){println("foo(B)")}
valb=B()
b.foo()//prints:foo(B)
(basA).foo()//1,prints:foo(A)
vala:A=b
a.foo()//1,prints:foo(A)
1. Herewewouldexpectfoo(B),whiletheobjectis,infact,oftypeB,butwhileextensionsareresolvedstatically,itisusinganextensionfunctionforA,becausethevariableisoftypeAandthereisnoinformationastowhatobjectisthereduringcompilation.
Thisfactissometimesproblematic,because,whenwedefineanextensionfunctiontothetypewearemostoftencastto,thenweshouldnotimplementextensionfunctionstoitssubclasses.
Thisisanimportantlimitation,andshouldbekeptinmind,especiallyduringpubliclibraryimplementation,becausethisway,someextensionfunctionscanblockothersandcauseunexpectedbehavior.
CompanionobjectextensionsIfaclasshasacompanionobjectdefined,thenyoucanalsodefineextensionfunctions(andproperties)forthiscompanionobject.Todistinguishbetweenanextensiontoaclassandanextensiontoacompanionobject,thereneedstobe.Companionaddedbetweentheextensiontypeandfunctionname:
classA{
companionobject{}
}
funA.Companion.foo(){print(2)}
Whenitisdefined,thefoomethodcanbeusedasifitweredefinedinsidetheAcompanionobject:
A.foo()
Notethatwearecallingthisextensionusingclasstype,notclassinstance.Toallowthecreationofanextensionfunctionforacompanionobject,thereneedstobeacompanionobjectexplicitlydefinedinsidetheclass.Evenanemptyone.Withoutit,itisimpossibletodefineanextensionfunction:
OperatoroverloadingusingextensionfunctionsOperatoroverloadingisabigKotlinfeature,butoftenweneedtouseJavalibrariesandoperatorsthatarenotdefinedthere.Forexample,inRxJava,weusetheCompositeDisposablefunctiontomanagesubscriptions.Thiscollectionusestheaddmethodtoaddnewelements.ThisisanexamplesubscriptionaddedtoCompositeDisposable:
valsubscriptions=CompositeDisposable()
subscriptions.add(repository
.getAllCharacters(qualifiedSearchQuery)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::charactersLoaded,view::showError))
ThestandardKotlinwaytoaddanewelementtoamutablecollectionisbyusingtheplusAssignoperator(+=).Itisnotonlymoreuniversal,butalsocleaner,whilewecanomitbrackets:
vallist=mutableListOf(1,2,3)
list.add(1)
list+=1
Toapplyitinourexample,wecanaddthefollowingextension:
operatorfunCompositeDisposable.plusAssign(disposable:Disposable)
{
add(disposable)
}
AndnowwecanusetheplusAssignmethodonCompositeDisposable:
subscriptions+=repository
.getAllCharacters(qualifiedSearchQuery)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::charactersLoaded,view::showError)
Whereshouldtop-levelextensionfunctionsbeused?Extensionfunctionsaremostoftenusedwhenwefeelthataclassdefinedbyotherprogrammersismissingsomemethod.Forexample,ifwethinkthatViewshouldcontainshowandhidemethods,usageforwhichwouldbeeasierthanvisibilityfieldsetting,thenwecanjustimplementitourselves:
funView.show(){visibility=View.VISIBLE}
funView.hide(){visibility=View.GONE}
Thereisnoneedtorememberthenamesofclassesthatholdutilfunctions.IntheIDE,wejustputadotaftertheobject,andwecansearchthroughallmethodsthatareprovidedtogetherwiththisobjectextensionfunctionsfromtheprojectandlibraries.Invocationlooksgood,whileitlookslikeanoriginalobjectmember.Thisisthebeautyofextensionfunctions,butitisalsoadanger.Rightnow,therearealreadytonsofKotlinlibrariesthatarejustpacksofextensionfunctions.Whenweuselotsofextensionfunctions,wecanmakeourAndroidcodeunlikenormalAndroidcode.Thishasbothprosandcons.Herearethepros:
CodeisshortandmorereadableCodepresentsmorelogicinsteadofAndroidboilerplateExtensionfunctionsaremostoftentested,oratleastusedinmultipleplaces,soitissimplertofindoutiftheyareworkingcorrectlyWhenweuseextensionfunctions,thereisasmallerchancethatwewillmakeastupiderrorthatwillleadtohoursofcodedebugging
Toillustratethelasttwopoints,wewillgobacktothetoastfunction.Itishardtomakeanerrorinwritingthefollowing:
toast("Sometext")
Whileitismucheasiertomakeanerrorinthefollowing:
Toast.makeText(this,"Sometext",Toast.LENGTH_LONG).show()
Thebiggestproblemwithstrongextensionusageinaprojectisthatweare,infact,makingourownAPI.Wearenamingandimplementingfunctionsandwedecidewhatargumentsshouldbethere.Whensomedeveloperjoinstheteam,heneedtolearntheentireAPI,we'vecreated.TheAndroidAPIhaslotsofshortcoming,butitsstrengthisthatitisuniversalanditisknowntoallAndroiddevelopers.
Doesthismeanweshouldresignfromextensions?Absolutelynot!Thisisagreatfeaturethatishelpingustomakecodeshortandclean.Thepointisthatweshouldusetheminasmartway:
Avoidmultipleextensionsthataredoingthesamething.Shortandsimplefunctionalityoftendoesn'tneedtobeanextension.Keeponecodingstylearoundtheproject.Talktoyourteamandspecifysomestandards.Becarefulwhenyouareusingpubliclibrarieswithextensions.KeepthemasthecodethatyoucannotchangeandmatchyourextensionstothemtokeeptheAPIclear.
ExtensionpropertiesInthissection,wewillfirstunderstandwhatextensionpropertiesare,andthenwewillmoveontolearnwherethesepropertiescanbeused.Aswealreadyknow,propertiesinKotlinaredefinedbytheiraccessors(getterandsetter):
classUser(valname:String,valsurname:String){
valfullName:String
get()="$name$surname"
}
Wecandefinealsoextensionproperty.Theonlylimitationisthatthispropertycan’thavebackingfield.Thereasonforthisisthatextensioncan’tstorestate,sothereisnogoodplacetostorethisfield.HereisanexampleofextensionpropertydefinitionforTextView:
valTextView.trimmedText:String
get()=text.toString().trim()
//Usage
textView.trimmedText
Aswithextensionfunctions,theaboveimplementationwillbecompiledasanaccessorfunctionwithareceiveronthefirstparameter.HereisthesimplifiedresultinJava:
publicclassAndroidUtilsKt{
StringgetTrimmedText(TextViewreceiver){
returnreceiver.getText().toString().trim();
}
}
Ifitwerearead-writeproperty,thenbothsetterandgetterwouldbeimplemented.Rememberthatonlypropertiesthatdon'tneedaJavafieldareallowedtobedefinedasanextensionproperty.Forexample,thisisillegal:
Whereshouldextensionpropertiesbeused?Extensionpropertiescanoftenbeusedinterchangeablywithextensionfunctions.Theyarebothmostoftenusedastop-levelutils.Extensionpropertiesareusedwhenwewouldlikeanobjecttohavesomepropertythatwasnotdevelopednatively.Thedecisionastowhetherweshoulduseanextensionfunctionoranextensionpropertyisnearlythesameasthedecisionastowhetherweshoulduseafunctionorpropertywithoutabackingfieldinsideaclass.Justtoremindyou,accordingtoconventions,oneshouldpreferapropertyoverafunctionwhentheunderlyingalgorithmfulfillsthefollowingconditions:
DoesnotthrowerrorsHasO(1)complexityIscheaptocalculate(orcaсhedonthefirstrun)Returnsthesameresultoverinvocations
Let'slookatasimpleproblem.WeoftenneedtogetsomeservicesinAndroid,butthecodeusedtogetthemiscomplicated:
PreferenceManager.getDefaultSharedPreferences(this)
getSystemService(Context.LAYOUT_INFLATER_SERVICE)asLayoutInflater
getSystemService(Context.ALARM_SERVICE)asAlarmManager
TouseaservicesuchasAlarmManagerorLayoutInflater,theprogrammerhastorememberthefollowingforeachofthem:
Thenameofthefunctionthatisprovidingit(suchasgetSystemService)andwhatclasscontainsit(suchasContext)Thenameofthefieldthatisspecifyingthisservice(suchasContext.ALARM_SERVICE)Thenameoftheclassthattheserviceshouldbecastto(suchasAlarmManager)
Thisiscomplex,andthisistheperfectplacewherewecanoptimizeusagethankstoextensionproperties.Wecandefineextensionpropertiesthisway:
valContext.preferences:SharedPreferences
get()=PreferenceManager
.getDefaultSharedPreferences(this)
valContext.inflater:LayoutInflater
get()=getSystemService(Context.LAYOUT_INFLATER_SERVICE)
asLayoutInflater
valContext.alarmManager:AlarmManager
get()=getSystemService(Context.ALARM_SERVICE)
asAlarmManager
Andfromnowon,wecanusepreferences,inflater,andalarmManagerasiftheyarepropertiesofContext:
context.preferences.contains("SomeKey")
context.inflater.inflate(R.layout.activity_main,root)
context.alarmManager.setRepeating(ELAPSED_REALTIME,triggerAt,
interval,pendingIntent)
Theseareperfectexamplesofgoodread-onlyextensionfunctionusage.Let'sfocusontheinflaterextensionproperty.Itishelpingtogetelementsthatareoftenneeded,buthardtogetwithoutextensions.Itishelpful,becausetheprogrammerjustneedstorememberthatwhattheyneedisaninflaterandthattheyneedContexttohaveit,andhedoesnotneedtorememberthenameofmethodthatisprovidingsystemservices(getSystemService),thenameofthekeyusedtogettheinflaterproperty(ALARM_SERVICE),whereitislocated(inContext),andwhatthisserviceshouldbecastto(AlarmManager).Inotherwords,thisextensionissavingalotofworkandprogrammermemory.Also,itiscorrectaccordingtoguidelines,becausethetimeofpropertygetterexecutionisshortanditscomplexityisO(1),itisnotthrowinganyerrors,anditisalwaysreturningthesameinflater(infact,itmightbeadifferentinstance,butfromaprogrammerperspective,itsusageisalwaysthesame,andthisiswhatisimportant).
We'veseenread-onlyextensionproperties,butwehavenotseenread-writeextensionproperties.Hereisagoodexample,thatisanalternativetothehideandshowfunctionsthatwesawintheExtensionfunctionssection:
varView.visible:Boolean
get()=visibility==View.VISIBLE
set(value){
visibility=if(value)View.VISIBLEelseView.GONE
}
Wecanchangethevisibilityoftheviewelementusingthisproperty:
button.visible=true//thesameasshow()
button.visible=false//thesameashide()
Also,wecancheckviewelementvisibility:
if(button.visible){/*...*/}
Oncewedefineit,wecantreatisasifitreallywereaViewproperty.Itisalsoimportantthatwhatwearesettingisconsistentwithwhatwearegetting.Sosupposingthatthereisnootherthreadwhichischangingelementvisibility,wecansetsomepropertyvalue:
view.visible=true
Thenthegetterwillalwaysprovidethesamevalue:
println(view.visible)//Prints:true
Finally,thereisnootherlogicinsidethegetterandsetter--onlyachangeinspecificproperties.Sootherconventionswe'vepresentedbeforearesatisfiedtoo.
MemberextensionfunctionsandpropertiesWe'veseentop-levelextensionfunctionsandproperties,butitisalsopossibletodefinetheminsideaclassorobject.Extensionsdefinedtherearecalledmemberextensions,andtheyaremostoftenusedfordifferentkindsofproblemsthantop-levelextensions.
Let'sstartfromthesimplestusecasewherememberextensionsareused.Let'ssupposethatweneedtodropeverythirdelementofalistofString.Hereistheextensionfunctionthatallowsustodropeveryithelement:
funList<String>.dropOneEvery(i:Int)=
filterIndexed{index,_->index%i==(i-1)}
Theproblemwiththatfunctionisthatitshouldnotbeextractedasautilextension,becauseofthefollowing:
Itisnotpreparedfordifferenttypesoflists(suchasalistofUser,orInt)Itisararelyusefulfunction,soprobablyitwon'tbeusedanywhereelseintheproject
Thisiswhywewouldwanttokeepitasprivate,anditisagoodideatokeepitinsidetheclasswhereweareusingit,asanmemberextensionfunction:
classUsersItemAdapter:ItemAdapter(){
lateinitvarusersNames:List<String>
funprocessList(){
usersNames=getUsersList()
.map{it.name}
.dropOneEvery(3)
}
funList<String>.dropOneEvery(i:Int)=
filterIndexed{index,_->index%i==(i-1)}
//...
}
Thisisthefirstreasonweusememberextensionfunctions,toprotecttheaccessibilityoffunctions.Inthiscase,itcouldbedonebydefiningafunctionon
thetoplevel,inthesamefile,andwithaprivatemodifier.Butmemberextensionfunctionsactdifferentlytotop-levelfunctions.Thefunctionusedintheprecedingcodeispublic,butitcanonlybecalledonList<String>andonlyinUsersItemAdapter.SoitcanbeusedonlyinsidetheUsersItemAdapterclassanditssubclassesorinsideanextensionfunctiontoUsersItemAdapter:
funUsersItemAdapter.updateUserList(newUsers:List<User>){
usersNames=newUsers
.map{it.name}
.dropOneEvery(3)
}
Notethattouseamemberextensionfunction,weneedboththeobjectinwhichitisimplementedandtheobjectonwhichthisextensionfunctionswillbecalled.Itisthiswaybecausewecanuseelementsofbothoftheseobjects.Thisisimportantinformationaboutmemberextensions:theycanusebothelementsfromreceivertypeandfrommembertypewithoutaqualifier.Let'sseehowitmightbeused.Hereisanotherexample,whichissimilartothepreviousone,butitisusingtheprivatepropertycategory:
classUsersItemAdapter(
privatevalcategory:Category
):ItemAdapter(){
lateinitvarusersNames:List<String>
funprocessList(){
usersNames=getUsersList()
.fromSameCategory()
.map{it.name}
}
funList<User>.fromSameCategory()=
filter{u->u.category.id==category.id}
privatefungetUsersList()=emptyList<User>()
}
InsidethememberextensionfunctionfromSameCategory,weareoperatingonanextensionreceiver(List<User>),butwearealsousingthecategorypropertyfromUsersItemAdapter.Weseeherethatafunctiondefinedthiswayneedstobeamethodanditcanbeusedsimilarlytoothermethods.TheadvantageoverthestandardmethodisthatwecancallafunctiononList,sowecankeepcleanstreamprocessing,insteadofnon-extensionmethodusage:
//fromSameCategorydefinedasstandardmethod
usersNames=fromSameCategory(newUsers)
.dropLast(3)
//fromSameCategorydefinedasmemberextensionfunction
usersNames=newUsers
.fromSameCategory()
.dropLast(3)
Anothercommonusageisthememberextensionfunctionsorpropertiescanbeusedlikenormalmethods,butweareusingthefactthatinsidememberfunctionswecanusereceiverpropertiesandmethodswithoutnamingthem,thiswaywecanhaveshortersyntax,andthatweareactuallycallingthemonareceiverinsteadofcallingthemwiththesametypeasanargument.Asanexample,wecantakethefollowingmethod:
privatefunsetUpRecyclerView(recyclerView:RecyclerView){
recyclerView.layoutManager
=LinearLayoutManager(recyclerView.context)
recyclerView.adapter
=MessagesAdapter(mutableListOf())
}
//Usage
setUpRecyclerView(recyclerView)
Thenwecanreplaceitwiththefollowingmemberextensionfunction:
privatefunRecyclerView.setUp(){
layoutManager=LinearLayoutManager(context)
adapter=MessagesAdapter(mutableListOf())
}
//Usage
recyclerView.setUp()
Usingmemberextensionfunctions,wecanachievebothasimplercallandasimplerfunctionbody.ThebiggestproblemwiththisattemptisthatitisnotclearwhichfunctionsweareusingaremembersofRecyclerView,andwhicharemembersoftheActivityandRecyclerViewextensions.Thisproblemwillberaisedinthenextpages.
TypeofreceiversWhenwehaveamemberextensionfunction,thenitbecomesmorecomplicatedtoadministerwhichelementswearecalling.Insideamemberextension,wehaveimplicitaccesstothefollowing:
Memberfunctionsandproperties,bothfromthisclassandsuperclassesReceivertypefunctionsandproperties,bothfromthereceivertypeanditssupertypesTop-levelfunctionsandproperties
SoinsidethesetUpextensionfunction,wecanusebothmemberandreceivermethodsandproperties:
classMainActivity:Activity(){
overridefunonCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
valbuttonView=findViewById(R.id.button_view)asButton
buttonView.setUp()
}
privatefunButton.setUp(){
setText("Clickme!")//1,2
setOnClickListener{showText("Hello")}//2
}
privatefunshowText(text:String){
toast(text)
}
}
1. setTextistheButtonclassmethod.2. WecanusetheButtonclassandMainActivityclassmembersalternately.
Itmightbetricky--probablymostpeoplewouldn'tnoticeiftherewereanerrorandthesetTextcallwouldbeswappedwiththeshowTextcall.
Whilewecanuseinsidememberextensionelementsfromdifferentreceivers,toallowdistinctionbetweenthem,allkindsofreceiverswerenamed.Firstofall,allobjectsthatcanbeusedbythethiskeywordarecalledimplicitreceivers.They'rememberscanbeaccessedwithoutaqualifier.InsidesetUpfunctions,
therearetwoimplicitreceivers:
Extensionreceiver:Aninstanceoftheclassthattheextensionisdefinedfor(Button)DispatchReceiver:Aninstanceoftheclassinwhichtheextensionisdeclared(MainActivity)
Notethatwhilemembersofboththeextensionreceiveranddispatchreceiverareimplicitreceiversinthesamebody,itispossibletohaveasituationwhereweusemembersthathavethesamesignatureinbothofthem.Forexample,ifwechangethepreviousclasstoshowtextintextViewinsteadofshowingitinthetoastfunction,andchangethemethodnametosetText,thenwearegoingtohavemethodsofdispatchandextensionreceiverwiththesamesignature(onedefinedintheButtonclass,theotherdefinedintheMainActivityclass):
classMainActivity:Activity(){
overridefunonCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
valbuttonView=findViewById(R.id.button_view)asButton
buttonView.setUp()
}
privatefunButton.setUp(){
setText("Clickme!")
setOnClickListener{setText("Hello")}//1
}
privatefunsetText(text:String){
textView.setText(text)
}
}
1. setTextisboththemethodofthedispatchreceiverandtheextensionreceiver.Whichonewillbecalled?
Asaresult,thesetTextfunctionwillbeinvokedfromtheextensionreceiver,andasaresult,abuttonclickwillchangethetextoftheclickedbutton!Thisisbecausetheextensionreceiveralwaystakesprecedenceoverthedispatchreceiver.Still,itispossibletouseadispatchreceiverinthissituationbyusingqualifiedthissyntax(thethiskeywordwithlabel,thatis,distinguishingwhichreceiverwewanttoreference):
privatefunButton.setUp(){
setText("Clickme!")
setOnClickListener{
this@MainActivity.setText("Hello")
}
}
Thisway,wecansolvetheproblemofdistinguishingbetweenthedispatchandextensionreceiver.
MemberextensionfunctionsandpropertiesunderthehoodMemberextensionfunctionsandpropertiesarecompiledthesamewayastop-levelextensionfunctionsandpropertieswiththeonlydifferencebeingthattheyareinsideaclassandtheyarenotstatic.Hereisasimpleexampleofanextensionfunction:
classA{
funboo(){}
funInt.foo(){
boo()
}
}
Thisiswhatitiscompiledto(aftersimplification):
publicfinalclassA{
publicfinalvoidboo(){
...
}
publicfinalvoidfoo(int$receiver){
this.boo();
}
}
Notethatwhiletheyarejustmethodswithareceiverasthefirstparameter,wecandowiththemeverythingwecanwithotherfunctions.Accessmodifiersareworkingthesameway,andifwedefinethememberextensionfunctionasopen,thenwecanoverrideitinitssubclasses.
GenericextensionfunctionsWhenwearewritingutilityfunctions,oftenwewantthemtobegeneric.Themostcommonexamplesareextensionsforcollections:List,Map,andSet.HereisanexampleofanextensionpropertyforList:
val<T>List<T>.lastIndex:Int
get()=size-1
Theprecedingexampledefinesanextensionpropertyforagenerictype.Thiskindofextensionisusedforlotsofdifferentproblems.Asanexample,startinganotherActivityisarepetitivetaskthatmostoftenneedstobeimplementedinmultipleplacesintheproject.ThemethodsprovidedbytheAndroidIDEforActivitystartingdonotmakeiteasy.HereisthecodeusedtostartanewActivitycalledSettingsActivity:
startActivity(Intent(this,SettingsActivity::class.java))
Notethatthissimpleandrepetitivetaskneedsalotofcodethatisnotreallyclear.ButwecandefineextensionfunctionsthatwillmakeIntentcreationandActivitywithoutargumentsstartmuchmoresimplyusingagenericinlineextensionfunctionwithreifiedtype:
inlinefun<reifiedT:Any>Context.getIntent()
=Intent(this,T::class.java)
inlinefun<reifiedT:Any>Context.startActivity()
=startActivity(getIntent<T>())
NowwecanstartActivitybysimplyusingthefollowing:
startActivity<SettingsActivity>()
Orwecancreateintentthisway:
valintent=getIntent<SettingsActivity>()
Thisway,wecanmakethiscommontaskeasieratlowcost.Togofurther,librariessuchasAnko(https://github.com/Kotlin/anko)provideextensionfunctionsthatprovideasimplewaytostartanActivitywithadditionalparametersorflags,asinthisexample:
startActivity<SettingsActivity>(userKeytouser)
Internalimplementationofthelibraryisoutsidethescopeofthisbook,butwecanusethisextensionsimplybyaddingAnkolibrarydependencytoourproject.Thepointofthisexampleisthatnearlyallrepetitivecodecanbereplacedwithsimplercodeusingextensions.TherearealsoalternativewaystostartanActivity,suchastheActivityStarterlibrary(https://github.com/MarcinMoskala/ActivityStarter),whichisbasedonparameterinjection,andthatstronglysupportsKotlin.Itallowsclassicargumentinjection:
classStudentDataActivity:BaseActivity(){
lateinit@Argvarstudent:Student
@Arg(optional=true)varlesson:Lesson=Lesson.default()
}
Or,asanalternative,itallowslazyinjectioninKotlinpropertydelegates(whicharedescribedinChapter8,Delegates):
classStudentDataActivity:BaseActivity(){
@get:Argvalstudent:StudentbyargExtra()
@get:Arg(optional=true)
varlesson:LessonbyargExtra(Lesson.default())
}
Activitywithsuchargumentscanbestartedusinggeneratedstaticfunctions:
StudentDataActivityStarter.start(context,student,lesson)
StudentDataActivityStarter.start(context,student)
Let'sseeanotherexample.InAndroid,weoftenneedtostoreobjectsinJSONformat.Forexample,whenweneedtosendthemtoanAPIortostoretheminafile.ThemostpopularlibraryusedforserializinganddeserializingobjectsintoJSONisGson.Let'slookatstandardwayofusingtheGsonlibrary:
valuser=User("Marcin","Moskala")
valjson:String=globalGson.toJson(user)
valuserFromJson=globalGson.fromJson(json,User::class.java)
WecanimproveitinKotlinthankstoextensionfunctionswithaninlinemodifier.HereisanexampleofextensionfunctionsthatareusingGSONtopackandunpackobjectstoStringinJSONformat:
inlinefunAny.toJson()=globalGson.toJson(this)!!
inlinefun<reifiedT:Any>String.fromJson()
=globalGson.fromJson(this,T::class.java)
//Usage
valuser=User("Marcin","Moskala")
valjson:String=user.toJson()
valuserFromJson:User=json.fromJson<User>()
TheglobalGsoninstanceisaglobalinstanceofGson.Itiscommonpractice,whileweoftendefinesomeserializersanddeserializers,anditisasimplerandmoreeffectivewaytodefinethemandbuildaninstanceofGsononce.
Examplesareshowingwhatpossibilitiesaregenericextensionfunctionsgivingtothedeveloper.Theyarelikethenextlevelofcodeextraction:
Theyaretop-level,butalsoinvokedonanobject,sotheyaresimpletomanageTheyaregeneric,soareuniversalandmightbeappliedtoanythingWheninline,theyallowustodefinereifiedtypeparameters
ThisiswhygenericextensionfunctionsarecommonlyusedinKotlin.Also,standardlibraryprovideslotsofgenericextensions.Inthenextsection,wewillseesomecollectionextensionfunctions.Thispartisimportant,notonlybecauseitprovidesknowledgeaboutgenericextensionfunctionusage,butalsobecauseitisultimatelydescribinghowlistprocessinginKotlinworksandhowitcanbeused.
CollectionprocessingCollectionprocessingisoneofthemostcommontasksinprogramming.Thisiswhyoneofthefirstthingsthatdeveloperslearnishowtoiterateoveracollectiontooperateonelements.Youngdevelopersaskedtoprintallusersfromalistwillmostprobablyuseaforloop:
for(userinusers){
println(user)
}
Ifweaskedthemtoshowonlyusersthatarepassinginschool,thentheywouldmostprobablyaddanifconditioninsidethisloop:
for(userinusers){
if(user.passing){
println(user)
}
}
Thisisstillcorrectimplementation,buttherealproblemstartswhentaskbecomesmorecomplex.Whatiftheywereaskedtoprintthethreebeststudentsthatarepassing?Itisreallycomplextoimplementitinloops,whileitistrivialtoimplementitusingKotlinstreamprocessing.Let'sseeitontheexample.Hereisexamplelistofstudents:
dataclassStudent(
valname:String,
valgrade:Double,
valpassing:Boolean
)
valstudents=listOf(
Student("John",4.2,true),
Student("Bill",3.5,true),
Student("John",3.2,false),
Student("Aron",4.3,true),
Student("Jimmy",3.1,true)
)
Let'sfilteroutstudentsusinganimperativeapproachknownfromJava(usingloopsandsortingmethod):
valfilteredList=ArrayList<Student>()
for(studentinstudents){
if(student.passing)filteredList+=student
}
Collections.sort(filteredList){p1,p2->
if(p1.grade>p2.grade)-1else1
}
for(iin0..2){
valstudent=filteredList[i]
println(student)
}
//Prints:
//Student(name=Aron,grade=4.3,passing=true)
//Student(name=John,grade=4.2,passing=true)
//Student(name=Bill,grade=3.5,passing=true)
WecanachievethesameresultinamuchsimplerwayusingKotlinstreamprocessing:
students.filter{it.passing}//1
.sortedByDescending{it.grade}//2
.take(3)//3
.forEach(::println)//4
1. Takeonlystudentswhopassed.2. Sortstudentsaccordingtotheirgrade(descendingtohavestudentswith
bettergradeinhigherposition).3. Takeonlyfirstthreeofthem.4. Printeachofthem.
Thekeyisthateachstreamprocessingfunction,suchassortedByDescending,take,andforEachfromtheprecedingexample,isextractingasmallfunctionalityandthepowercomesfromthecompositionofthem.Andtheresultismuchsimplerandmorereadablethenusageofclassicloops.
Streamprocessingisactuallyaprettycommonlanguagefeature.ItisknowninC#,JavaScript,Scala,andmanyotherlanguages,includingJavasinceversion8.Popularreactiveprogramminglibraries,suchasRxJava,alsoheavilyutilizethisconcepttoprocessdata.Inthissection,wearegoingtogodeeperintoKotlincollectionprocessing.
KotlincollectiontypehierarchyKotlintypehierarchyisreallywelldesigned.Standardcollectionsareactuallycollectionsfromanativelanguage(suchasJava),whicharehiddenbehindinterfaces.Creationofthemismadebystandardtop-levelfunctions(listOf,setOf,mutableListOf,andsoon),sotheycanbecreatedandusedincommonmodules(modulescompiledtomorethanoneplatform).AlsoKotlininterfacescanactliketheirequivalentinterfacesfromJava(likeList,Set,andsoon),thismakesKotlincollectionsefficientandhighlycompatiblewithexternallibraries.Atthesametime,Kotlincollectioninterfaceshierarchy,canbeusedincommonmodules.Thishierarchyissimpleanditisprofitabletounderstandit:
Kotlincollectioninterfaceshierarchy
ThemostgeneralinterfaceisIterable.Itrepresentsasequenceofelementsthatcanbeiteratedover.Anyobjectthatimplementsiterablecanbeusedinaforloop:
for(iiniterable){/*...*/}
Lotsofdifferenttypesimplementaniterableinterface:allcollections,progressions(1..10,'a'..'z'),andevenString.Theyallallowustoiterateovertheirelements:
for(charin"Text"){print("($char)")}//Prints:(T)(e)(x)(t)
TheCollectioninterfacerepresentsacollectionofelementsandextendsIterable.Itaddspropertysizeandthemethodscontains,containsAll,andisEmpty.
TwomaininterfacesthatinheritfromCollectionareListandSet.ThedifferencebetweenthemisthatSetisunorderedanddoesnotcontainrepetitiveelements(accordingtotheequalsmethod).BothListandSetinterfacesdonotcontainanymethodsthatwouldallowustomutatetheobjectstate.Thisiswhy,bydefault,Kotlincollectionsaretreatedasimmutable.WhenwehaveaninstanceofList,thenitismostoftenArrayListinAndroid.ArrayListisamutablecollection,butwhileitishiddenbehindtheinterfaceList,itisactuallyactinglikeimmutable,becauseitisnotexposinganymethodsthatwouldallowustoapplychanges(unlessitisdowncasted).
InJava,collectionsweremutable,butKotlincollectioninterfacesprovideonlyimmutablebehaviorbydefault(notmethodsthatchangethestateofcollections,forexample,addandremoveAt):
vallist=listOf('a','b','c')
println(list[0])//Prints:a
println(list.size)//Prints:3
list.add('d')//Error
list.removeAt(0)//Error
Allimmutableinterfaces(Collection,List,andsoon)havetheirmutableequivalents(MutableCollection,MutableList,andsoon)whichinheritfromcorrespondingimmutableinterfaces.Mutablemeansthattheactualobjectcanbemodified.Thesearetheinterfacesthatrepresentmutablecollectionsfromstandardlibrary:
MutableIterableallowsiterationwithapplyingchangesMutableCollectionensuremethodsforaddingandremovingelementsMutableListandMutableSetaremutableequivalentsofListandSet
Nowwecanfixourpreviousexampleandchangethecollectionusingtheaddandremovemethods:
vallist=mutableListOf('a','b','c')
println(list[0])//Prints:a
println(list.size)//Prints:3
list.add('d')
println(list)//Prints:[a,b,c,d]
list.removeAt(0)
println(list)//Prints:[b,c,d]
Bothimmutableandmutableinterfacesprovideonlyafewmethods,buttheKotlinstandardlibraryprovidesmanyusefulextensionsforthem:
ThismakesdealingwithcollectionsamucheasiertaskthaninJava.
Kotlinimplementscollectionprocessingmethodsusingextensions.Thisapproachhasmanyadvantages;forexample,ifwewanttoimplementacustomcollection(suchasList),weonlyneedtoimplementaniterableinterfacecontainingonlyafewmethods.Wecanstillusealltheextensionsthatareprovidedfortheiterableinterface.
Anotherreasonishowflexiblythesefunctionscanbeusedwhentheyareextensionsforinterfaces.Forexample,mostofthesecollectionprocessingfunctionsareactuallyextensionsforIterable,whichisimplementedbymanymoretypesthanCollection,forexample,byStringorRange.Therefore,itis
possibletouseallextensionfunctionstoIterablealsoonIntRange.Hereisanexample:
(1..5).map{it*2}.forEach(::print)//Prints:246810
Thismakesallthisextensionsreallyuniversal.Thereisalsothedownsideofthefactthatcollectionstreamprocessingmethodsareimplementedasextensionfunctions.Whileextensionsareresolvedstatically,itisincorrecttooverrideanextensionfunctionforaspecifictypebecauseitsbehaviorwillbedifferentwhenitisbehindaninterfacethenwhenitisaccesseddirectly.
Let'sanalyzesomeextensionfunctionsusedforcollectionprocessing.
Themap,filter,flatMapfunctionsWehavealreadybrieflypresentedmap,filter,andflatMap,becausetheyarethemostbasicstreamprocessingfunctions.Themapfunctionreturnsalistwithelementschangedaccordingtothefunctionfromtheargument:
vallist=listOf(1,2,3).map{it*2}
println(list)//Prints:[2,4,6]
Thefilterfunctionallowsonlytheelementsthatmatchtheprovidedpredicate:
vallist=listOf(1,2,3,4,5).map{it>2}
println(list)//Prints:[3,4,5]
TheflatMapfunctionreturnsasinglelistofallelementsyieldedbythetransformfunction,whichisinvokedoneachelementoftheoriginalcollection:
vallist=listOf(10,20).flatMap{listOf(it,it+1,it+2)}
println(list)//Prints:[10,11,12,20,21,22]
Itismostoftenusedtoflattenlistofcollections:
shops.flatMap{it.products}
schools.flatMap{it.students}
Let'slookatsimplifiedimplementationsoftheseextensionfunctions:
inlinefun<T,R>Iterable<T>.map(transform:(T)->R):List<R>{//1
valdestination=ArrayList<R>()
for(iteminthis)destination.add(transform(item))//2
returndestination
}
inlinefun<T>Iterable<T>.filter(predicate:(T)->Boolean):List<T>{//1
valdestination=ArrayList<T>()
for(iteminthis)if(predicate(item))destination.add(item)//2
returndestination
}
inlinefun<T,R>Iterable<T>.flatMap(transform:(T)->Collection<R>):List<R>{
//1
valdestination=ArrayList<R>()
for(iteminthis)destination.addAll(transform(item))//2
returndestination
}
1. Allthisfunctionsareinline.
2. Allthesefunctionsinternallyuseforloop,andreturnanewlistcontainingproperelements.
MostKotlinstandardlibraryextensionfunctionswithfunctiontypeareinline,becauseitmakeslambdaexpressionusageefficient.Asaresult,wholecollectionstreamprocessingisactuallymostlycompiledatruntimetonestedloops.Asanexample,hereisthissimpleprocessing:
students.filter{it.passing}
.map{"${it.name}${it.surname}"}
AftercompilationanddecompilationtoJava,itlookslikethefollowing(cleanedup):
Collectiondestination1=newArrayList();
Iteratorit=students.iterator();
while(it.hasNext()){
Studentstudent=(Student)it.next();
if(student.getPassing()){
destination1.add(student);
}
}
Collectiondestination2=newArrayList(destination1.size());
it=destination2.iterator();
while(it.hasNext()){
Studentstudent=(Student)it.next();
Stringvar=student.getName()+""+student.getSurname();
destination2.add(var);
}
TheforEachandonEachfunctionsTheforEachfunctionwasalreadydiscussedinchapteraboutfunctions.Itisanalternativetoaforloop,soitperformsanactiononeachelementofthelist:
listOf("A","B","C").forEach{print(it)}//prints:ABC
SinceKotlin1.1,thereisasimilarfunction,onEach,thatalsoinvokesanactiononeachelement.Itreturnsanextensionreceiver(thislist),sowecaninvokeanactiononeachelementinthemiddleofstreamprocessing.Commonusecasesareloggingpurposes.Hereisanexample:
(1..10).filter{it%3==0}
.onEach(::print)//Prints:369
.map{it/3}
.forEach(::print)//Prints:123
ThewithIndexandindexedvariantsSometimes,thewayofelementprocessingdependsonitsindexonthelist.ThemostuniversalwaytosolvethisproblemisbyusingthewithIndexfunction,whichreturnsalistofvalueswithindexes:
listOf(9,8,7,6).withIndex()//1
.filter{(i,_)->i%2==0}//2
.forEach{(i,v)->print("$vat$i,")}
//Prints:9at0,7at2,
1. FunctionwithIndexispackingeachelementintoIndexedValuewhichiscontainingboththeelementsanditsindex.
2. Inlambda,IndexedValueisdestructedintoindexandvalue,butwhilethevalueisunused,thereisanunderscoreplacedinstead.Itmightbeomitted,butthiswayofcodeismorereadable.Thislinefiltersonlyelementswithevenindex.
Also,therearevariantsfordifferentstreamprocessingmethodsthatprovideanindex:
vallist1=listOf(2,2,3,3)
.filterIndexed{index,_->index%2==0}
println(list1)//Prints:[2,3]
vallist2=listOf(10,10,10)
.mapIndexed{index,i->index*i}
println(list2)//Prints:[0,10,20]
vallist3=listOf(1,4,9)
.forEachIndexed{index,i->print("$index:$i,")}
println(list3)//Prints:0:1,1:4,2:9
Thesum,count,min,max,andsortedfunctionsThesumfunctioncountsthesumofallelementsinalist.ItcanbeinvokedonList<Int>,List<Long>,List<Short>,List<Double>,List<Float>,andList<Byte>:
valsum=listOf(1,2,3,4).sum()
println(sum)//Prints:10
Oftenweneedtosumsomepropertiesofelements,suchassummingpointsofallusers.Itmightbehandledbymappingthelistofuserstothelistofpointsandthencountingthesum:
classUser(valpoints:Int)
valusers=listOf(User(10),User(1_000),User(10_000))
valpoints=users.map{it.points}.sum()
println(points)//Prints:11010
Butweunnecessarilycreateanintermediatecollectionbycallingthemapfunction,anditwouldbemoreefficienttodirectlysumpoints.Todoit,wecanusesumBywithanappropriateselector:
valpoints=users.sumBy{it.points}
println(points)//Prints:11010
sumByisexpectingInttobereturnedfromtheselector,anditisreturningIntwiththesumofallelements.IfvaluesarenotIntbutDoublethenwecanusesumByDouble,whichreturnsDouble:
classUser(valpoints:Double)
valusers=listOf(User(10.0),User(1_000.0),User(10_000.0))
valpoints=users.sumByDouble{it.points}
println(points)//Prints:11010.0
Asimilarfunctionalityisprovidedbythecountfunction,thatisusedwhenweneedtocountelementsthatmatchapredicate:
valevens=(1..5).count{it%2==1}
valodds=(1..5).count{it%2==0}
println(evens)//Prints:3
println(odds)//Prints:2
Thecountfunctionusedwithoutanypredicatereturnsthesizeofthecollectionoriterable:
valnums=(1..4).count()
println(nums)//Prints:4
Thenextimportantfunctionsareminandmax,whicharefunctionsthatreturntheminimalandmaximalelementsinalist.Theycanbeusedonalistofelementsthathavenaturalordering(implementComparable<T>interface).Hereisanexample:
vallist=listOf(4,2,5,1)
println(list.min())//Prints:1
println(list.max())//Prints:5
println(listOf("kok","ada","bal","mal").min())//Prints:ada
Similarly,thefunctionsortedisused.Itreturnsasortedlist,butitneedstobeinvokedoncollectionsofelementsthatimplementtheComparable<T>interface.Hereisanexampleofhowsortedcanbeusedtogetalistofstringssortedalphanumerically:
valstrs=listOf("kok","ada","bal","mal").sorted()
println(strs)//Prints:[ada,bal,kok,mal]
Whatifitemsarenotcomparable?Therearetwowaystosortthem.Thefirstwayistosortaccordingtocomparablemember.We'vealreadyseenanexamplewhenweweresortingstudentsaccordingtotheirgrades:
students.filter{it.passing}
.sortedByDescending{it.grade}
.take(3)
.forEach(::println)
Intheprecedingexample,wesortstudentsusingthecomparablegradeproperty.There,sortedByDescendingisused,whichworkslikesortedBy,withtheonlydifferencebeingthattheorderisdescending(frombiggesttosmallest).Theselectorinsidethefunctioncanreturnanyvaluethatiscomparabletoitself.Hereisanexample,whereStringisusedtospecifyorder:
vallist=listOf(14,31,2)
print(list.sortedBy{"$it"})//Prints:[14,2,31]
Similarfunctionscanbeusedtofindtheminimalandmaximalelementaccordingtotheselector:
valminByLen=listOf("ppp","z","as")
.minBy{it.length}
println(minByLen)//Prints:"z"
valmaxByLen=listOf("ppp","z","as")
.maxBy{it.length}
println(maxByLen)//Prints:"ppp"
ThesecondwaytospecifysortingorderistodefineaComparatorthatwilldeterminehowelementsshouldbecompared.FunctionvariantsthatacceptcomparatorsshouldhaveaWithsuffix.ComparatorscanbedefinedbyanadapterfunctionthatconvertsalambdatoSAMtype:
valcomparator=Comparator<String>{e1,e2->
e2.length-e1.length
}
valminByLen=listOf("ppp","z","as")
.sortedWith(comparator)
println(minByLen)//Prints:[ppp,as,z]
Kotlinalsoincludesstandardlibrarytop-levelfunctions(compareBy,compareByDescending)usedtosimplifyComparatorcreation.Hereishowwecancreateacomparatortosortstudentsalphanumericallybysurnameandname:
dataclassUser(valname:String,valsurname:String){
overridefuntoString()="$name$surname"
}
valusers=listOf(
User("A","A"),
User("B","A"),
User("B","B"),
User("A","B")
)
valsortedUsers=users
.sortedWith(compareBy({it.surname},{it.name}))
print(sortedUsers)//[AA,BA,AB,BB]
Notethatwecanusepropertyreferenceinsteadoflambdaexpressions:
valsortedUsers=users
.sortedWith(compareBy(User::surname,User::name))
print(sortedUsers)//[AA,BA,AB,BB]
AnotherimportantfunctionisgroupBy,whichgroupselementsaccordingtotheselector.groupByreturnsMap,thatismappingfromthechosenkeytoalistofelementsthatareselectedtomaptothefollowingkey:
valgrouped=listOf("ala","alan","mulan","malan")
.groupBy{it.first()}
println(grouped)//Prints:{'a':["ala","alan"],"m":["mulan","malan"]}
Let'slookatamorecomplexexample.Weneedtogetalistofthebeststudentsfromeachclass.Hereishowwecangetthemfromthelistofstudents:
classStudent(valname:String,valclassCode:String,valmeanGrade:Float)
valstudents=listOf(
Student("Homer","1",1.1F),
Student("Carl","2",1.5F),
Student("Donald","2",3.5F),
Student("Alex","3",4.5F),
Student("Marcin","3",5.0F),
Student("Max","1",3.2F)
)
valbestInClass=students
.groupBy{it.classCode}
.map{(_,students)->students.maxBy{it.meanGrade}!!}
.map{it.name}
print(bestInClass)//Prints:[Max,Donald,Marcin]
OtherstreamprocessingfunctionsTherearelotsofdifferentstreamprocessingfunctionsandthereisnoneedtodescribethemallhere,whileKotlincontainsgreatdocumentationonitswebsite.Thenamesofmostoftheextensionfunctionsareself-explanatoryandthereisnoneedtoreallyreadthedocumentationtoguesswhattheyaredoing.InAndroidStudio,wecanchecktherealimplementationbypressingCtrl(commandkeyonmac)andclickingthefunctionwhoseimplementationwewanttoread.
Theimportantdifferenceincollectionprocessingcomeswhenyouareoperatingonmutablecollections,becausewhiletheycanuseadditionalextensionsdefinedformutabletypes(MutableIterable,andMutableCollection),theimportantdistinctionisthatfunctionsthatarechangingobjectareformulatedinpresentimperativeform(forexample,sort),whilefunctionsthatarereturninganewcollectionwithchangedvaluesaremostoftenformulatedinthepastformofaverb(forexample,sorted).Hereisanexample:
sort:Functionthatissortingamutableobject.ItreturnsUnit.sorted:Functionthatisreturningasortedcollection.Itisnotchangingthecollectiononwhichitisinvoked.
vallist=mutableListOf(3,2,4,1)
vallist2=list.sorted()
println(list)//[3,2,4,1]
println(list2)//[1,2,3,4]
list.sort()
println(list)//[1,2,3,4]
ExamplesofstreamcollectionprocessingWe'vealreadyseenafewstreamprocessingfunctions,butitneedssomeskillandcreativitytousethemforcomplexusecases.Thisiswhy,inthispart,wearegoingtodiscusssomecomplexstreamprocessingexamples.
Let'ssupposethatweagainneedtofindthebestthreestudentswhoarepassingaccordingtotheirgrade.Thekeydifferenceisthat,inthiscase,thefinalorderofstudentsmustbethesameasitwasinthebeginning.Notethatduringsortingbygradeoperation,thisorderislost.Butwecanpreserveitifwekeeptogethervalueandindex.Thankstothat,wecanlatersortelementsaccordingtothispreservedindex.Hereishowtoimplementthisprocessing:
dataclassStudent(
valname:String,
valgrade:Double,
valpassing:Boolean
)
valstudents=listOf(
Student("John",4.2,true),
Student("Bill",3.5,true),
Student("John",3.2,false),
Student("Aron",4.3,true),
Student("Jimmy",3.1,true)
)
valbestStudents=students.filter{it.passing}//1
.withIndex()//2
.sortedBy{it.value.grade}//3
.take(3)//4
.sortedBy{it.index}//5
.map{it.value}//6
//Printlistofnames
println(bestStudents.map{it.name})//[John,Bill,Jimmy]
1. Filtertokeeponlystudentsthatarepassing2. Addindextoelementstobeabletoreproduceelementorder3. Sortstudentsaccordingtotheirgrade4. Takeonlybest10students5. Reproduceorderbysortingaccordingtoindexes6. Mappingvalueswithindexestojustvalues
Notethatthisimplementationisconciseandeachoperationperformedonthecollectioniseasytoreadlinebyline.
Thebigadvantageofcollectionstreamprocessingisthatitiseasytomanagethecomplexityofthisprocess.Weknowthatthecomplexityofmostoperations,suchasmaporfilter,isO(n)andthecomplexityofsortingoperationsisO(n*log(n)).Thecomplexityofstreamoperationsismaximalcomplexityofeachofthesteps,sothecomplexityoftheaboveprocessingisO(n*log(n))becausesortedByisthestepwiththebiggestcomplexity.
Asthenextexample,let'ssupposethatwehavealistcontainingtheresultsofplayersindifferentcategories:
classResult(
valplayer:Player,
valcategory:Category,
valresult:Double
)
classPlayer(valname:String)
enumclassCategory{SWIMMING,RUNNING,CYCLING}
Andwehavesomeexampledata:
valresults=listOf(
Result("Alex",Category.SWIMMING,23.4),
Result("Alex",Category.RUNNING,43.2),
Result("Alex",Category.CYCLING,15.3),
Result("Max",Category.SWIMMING,17.3),
Result("Max",Category.RUNNING,33.3),
Result("Bob",Category.SWIMMING,29.9),
Result("Bob",Category.CYCLING,18.0)
)
Hereishowwecanfindthebestplayerineachcategory:
valbestInCategory=results.groupBy{it.category}//1
.mapValues{it.value.maxBy{it.result}?.player}//2
print(bestInCategory)
//Prints:{SWIMMING=Bob,RUNNING=Alex,CYCLING=Bob}
1. Wegroupresultsintocategories.ThereturntypeisMap<Category>,andList<Result>.
2. Wearemappingvaluesofthemapfunction.Inside,wefindthebestresultinthiscategoryandwearetakingtheplayerwhoisassociatedwiththisresult.ThereturnofthemapValuesfunctionisMap<Category,Player?>.
TheprecedingexampleshowshowcomplexproblemsrelatedtocollectionscanbeeasilysolvedinKotlinthankstocollectionprocessingfunctions.AfterworkingwithKotlinforawhile,mostofthosefunctionsarewellknowntoprogrammers,andthencollectionprocessingproblemsarequiteeasytosolve.Ofcourse,functionsascomplicatedaspresentedabovearerare,butsimple,few-stepprocessingisquitecommonineverydayprogramming.
SequenceSequenceisaninterfacethatisalsousedtorefertoacollectionofelements.ItisanalternativeforIterable.ForSequence,thereareseparateimplementationsofmostcollectionprocessingfunctions(map,flatMap,filter,sorted,andsoon).Thekeydifferenceisthatallthesefunctionsareconstructedinsuchawaythat,theyreturnsequence,whichispackagedoverprevioussequence.Duetothis,thefollowingpointsbecomestrue:
ThesizeofsequencedoesnotneedtobeknowninadvanceSequenceprocessingismoreefficient,especiallyforlargecollectionswherewewanttoperformseveraltransformations(detailswillbedescribedlater)
InAndroid,sequencesareusedforprocessingverybigcollectionsorforprocessingelementswhosesizeisnotknowninadvance(suchasforreadinglinesofpossiblylongdocument).Therearedifferentwaystocreatesequences,buttheeasiestistheasSequencefunctioncalledonIterableorbyusingthesequenceOftop-levelfunctiontomakesequencesimilarlyaslist.
Sequencesizedoesnotneedtobeknowninadvance,becausevaluesarecalculatedjustwhentheyareneeded.Hereisanexample:
val=generateSequence(1){it+1}//1.InstanceofGeneratorSequence
.map{it*2}//2.InstanceofTransformingSequence
.take(10)//3.InstanceofTakeSequence
.toList()//4.InstanceofList
println(numbers)//Prints:[2,4,6,8,10,12,14,16,18,20]
1. ThefunctiongenerateSequenceisonewayforsequencegeneration.Thissequencecontainsthenextnumbersfrom1toinfinity.
2. Themapfunctionpacksasequenceintoanotherthattakesthevaluefromthefirstsequenceandthenitcalculatesthevalueaftertransformation.
3. Thefunctiontake(10)willalsopackasequenceintoanotheronethatisfinishingonthe10thelement.Withoutthislineexecution,processingtime
wouldbeinfinitivewhileweareoperatingonaninfinitesequence.4. Finally,thefunctiontoListisprocessingeachvalueanditreturnsthefinal
list.
Itisimportanttostressthatelementsareprocessedoneafteranotherinthelaststep(interminaloperation).Let'slookatanotherexample,whereeveryoperationisalsoprintingvaluesforloggingpurposes.Let'sstartwiththefollowingcode:
valseq=generateSequence(1){println("Generated${it+1}");it+1}
.filter{println("Processingoffilter:$it");it%2==1}
.map{println("Processingmap:$it");it*2}
.take(2)
Whatwouldbeprintedintheconsole?Absolutelynothing.Novalueswerecalculated.Thereasonisthatallthoseintermediateoperationsarelazy.Toretrievearesult,weneedtousesometerminaloperation,suchastoList.Let'susethefollowing:
seq.toList()
Thenwewillseethefollowingintheconsole:
Processingoffilter:1
Processingmap:1
Generated2
Processingoffilter:2
Generated3
Processingoffilter:3
Processingmap:3
Noticethatelementsarefullyprocessedoneafteranother.Instandardlistprocessing,theorderofoperationwouldbetotallydifferent:
(1..4).onEach{println("Generated$it")}
.filter{println("Processingfilter:$it");it%2==1}
.map{println("Processingmap:$it");it*2}
Theprecedingcodeprintsthefollowing:
Generated1
Generated2
Generated3
Generated4
Processingfilter:1
Processingfilter:2
Processingfilter:3
Processingfilter:4
Processingmap:1
Processingmap:3
Thisexplainswhysequencesaremoreefficientthanclassiccollectionprocessing--thereisnoneedtocreatecollectionsinintermediatesteps.Valuesareprocessedonebyoneondemand.
FunctionliteralswithreceiverJustasfunctionshaveafunctiontype,whichallowsthemtobekeptasanobject,extensionfunctionshavetheirtypethatallowsthemtobekeptthisway.Itiscalledfunctiontypewithreceiver.Itlookslikethesimplefunctiontype,butthereceivertypeislocatedbeforearguments(likeinextensiondefinition):
varpower:Int.(Int)->Int
Theintroductionoffunctiontypewithreceivermakesfullcohesionbetweenfunctionsandtypes,becauseallfunctionscanbenowrepresentedasobjects.Itcanbedefinedusingalambdaexpressionwithreceiverorbyananonymousfunctionwithreceiver.
Inalambdaexpressionwithreceiverdefinition,theonlydifferenceisthatwecanreferencetoreceiverbythis,andwecanexplicitlyusereceiverelements.Forlambdaexpressions,thetypemustbespecifiedinaparameter,becausethereisnosyntaxtospecifyreceivertype.Hereispowerdefinedasalambdaexpressionwithreceiver:
power={n->(1..n).fold(1){acc,_->this*acc}}
Ananonymousfunctionalsoallowsustodefinethereceiver,anditstypeisplacedbeforethefunctionname.Insuchafunction,wecanusethisinsidethebodytorefertotheextensionreceiverobject.Notethatanonymousextensionfunctionsarespecifyingthereceivertype,sothepropertytypecanbeinferred.Hereispowerdefinedasananonymousextensionfunction:
power=funInt.(n:Int)=(1..n).fold(1){acc,_->this*acc}
Afunctiontypewithreceivercanbeusedasifitisamethodofreceivertype:
valresult=10.power(3)
println(result)//Prints:1000
Afunctiontypeismostoftenusedasafunctionparameter.Hereisanexample,whereaparameterfunctionisusedtoconfigureanelementafteritscreation:
funViewGroup.addTextView(configure:TextView.()->Unit){
valview=TextView(context)
view.configure()
addView(view)
}
//Usage
vallinearLayout=findViewById(R.id.contentPanel)asLinearLayout
linearLayout.addTextView{//1
text="Marcin"//2
textSize=12F//2
}
1. Hereweareusingalambdaexpressionasanargument.2. Insidethelambdaexpression,wecandirectlyinvokereceivermethods.
KotlinstandardlibraryfunctionsTheKotlinstdlibprovideasetofextensionfunctions(let,apply,also,with,run,andto)withgenericnon-restrictedreceiver(generictypeshavenorestrictions).Theyaresmallandhandyextensions,anditisveryprofitabletounderstandthem,becausetheyareveryusefulacrossallKotlinprojects.Oneofthesefunctions,let,wasbrieflyintroducedinChapter2,LayingaFoundation,wherewesawhowitcanbeusedasanalternativetoanullitycheck:
savedInstanceState?.let{state->
println(state.getBoolean("isLocked"))
}
Allthatletdoesisitcallsthespecifiedfunctionandreturnsitsresult.Whileintheaboveexampleitisusedtogetherwithasafecalloperator,itwillbecalledonlywhenthepropertysavedInstanceStateisnotnull.Theletfunctionisactuallyjustagenericextensionfunctionwithaparameterfunction:
inlinefun<T,R>T.let(block:(T)->R):R=block(this)
Instdlib,therearemorefunctionssimilartolet.Thesefunctionsareapply,also,with,andrun.Theyareasimilarsowearegoingtodescribethemtogether.Herearedefinitionsoftherestofthefunctions:
inlinefun<T>T.apply(block:T.()->Unit):T{
block();
returnthis
}
inlinefun<T>T.also(block:(T)->Unit):T{
block(this);
returnthis
}
inlinefun<T,R>T.run(block:T.()->R):R=block()
inlinefun<T,R>with(receiver:T,block:T.()->R):R=receiver.block()
Let'sseeusageexamples:
valmutableList=mutableListOf(1)
valmutableList=mutableListOf(1)
valletResult=mutableList.let{
it.add(2)
listOf("A","B","C")
}
println(letResult)//Prints:[A,B,C]
valapplyResult=mutableList.apply{
add(3)
listOf("A","B","C")
}
println(applyResult)//Prints:[1,2,3]
valalsoResult=mutableList.also{
it.add(4)
listOf("A","B","C")
}
println(alsoResult)//Prints:[1,2,3,4]
valrunResult=mutableList.run{
add(5)
listOf("A","B","C")
}
println(runResult)//Prints:[A,B,C]
valwithResult=with(mutableList){
add(6)
listOf("A","B","C")
}
println(withResult)//Prints:[A,B,C]
println(mutableList)//Prints:[1,2,3,4,5,6]
Thedifferencesaresummarizedinthefollowingtable:
Returnedobject/parameterfunctiontype
Functionliteralwithreceiver
(receiverobjectrepresentedasthis)
Functionliteral
(receiverobjectrepresentedasit)
Receiverobject apply also
Resultoffunctionliteral run/with let
Whilethosefunctionsaresimilarand,inmanycases,itispossibletousetheminterchangeably,thereareconventionswhichdefinewhichfunctionsarepreferredforcertainusecases.
TheletfunctionTheletfunctionispreferredwhenwewanttousestandardfunctionsasiftheyareextensionfunctionsinstreamprocessing:
valnewNumber=number.plus(2.0)
.let{pow(it,2.0)}
.times(2)
Likeotherextensions,itcanbecombinedwithasavecalloperator:
valnewNumber=number?.plus(2.0)
?.let{pow(it,2.0)}
Theletfunctionisalsopreferredwhenwejustwanttounpackanullableread-writeproperty.Inthissituation,itisnotpossibletosmartcastthispropertyandweneedtoshadowit,likeinthissolution:
varname:String?=null
funContext.toastName(){
valname=name
if(name!=null){
toast(name)
}
}
Thenamevariableistheshadowingpropertyname,whatisnecessaryifnameisaread-writeproperty,becausesmartcastisallowedonlyonamutableorlocalvariable.
Wecanreplacetheprecedingcodewithletusageandasafecalloperator:
name?.let{setNewName(it)}
NotethatusingElvisoperator,wecaneasilyaddareturnorexceptionthrowwhennameisnull:
name?.let{setNewName(it)}?:throwError("Nonamesetten")
Similarway,letcanbeusedasareplacementforthefollowingstatement:
valcomment=if(field==null)getComment(field)else"Nocomment
Implementationthatisusingtheletfunctionwouldlooklikethefollowing:
valcomment=field?.let{getComment(it)}?:"Nocomment"
Theletfunctionusedthiswayispreferredinmethodchainsthattransformthereceiver:
valtext="hello{name}"
funcorrectStyle(text:String)=text
.replace("hello","hello,")
fungreet(name:String){
text.replace("{name}",name)
.let{correctStyle(it)}
.capitalize()
.let{print(it)}
}
//Usage
greet("reader")//Prints:Hello,reader
Wecanalsousesimplersyntaxbypassingafunctionreferenceasanargument:
text.replace("{name}",name)
.let(::correctStyle)
.capitalize()
.let(::print)
UsingtheapplyfunctionforinitializationSometimesweneedtocreateandinitializeanobjectbycallingsomemethodsormodifyingsomeproperties,suchaswhenwearecreatingaButton:
valbutton=Button(context)
button.text="Clickme"
button.isVisible=true
button.setOnClickListener{/*...*/}
this.button=button
Wecanreducecodeverbositybyusingtheapplyextensionfunction.Wecancallallthesemethodsfromthecontextwherebuttonisthereceiverobject:
button=Button(context).apply{
text="Clickme"
isVisible=true
setOnClickListener{/*...*/}
}
ThealsofunctionThealsofunctionissimilartoapply,withtheonlydifferencebeingthattheparameterfunctionacceptsanargumentasanparameterratherthanasanreceiver.Itispreferredwhenwewanttodosomeoperationsonanobject,whicharenotinitializations:
abstractclassProvider<T>{
varoriginal:T?=null
varoverride:T?=null
abstractfuncreate():T
funget():T=override?:original?:create().also{original=it}
}
Thealsofunctionisalsopreferredwhenweneedtodosomeoperationinthemiddleofprocessing,forexample,duringobjectconstructionusingtheBuilderpattern:
funmakeHttpClient(vararginterceptors:Interceptor)=
OkHttpClient.Builder()
.connectTimeout(60,TimeUnit.SECONDS)
.readTimeout(60,TimeUnit.SECONDS)
.also{it.interceptors().addAll(interceptors)}
.build()
Anothersituationwherealsoispreferrediswhenwearealreadyinanextensionfunctionandwedon'twanttoaddanotherextensionreceiver:
classSnail{
varname:String=""
vartype:String=""
fungreet(){
println("Hello,Iam$name")
}
}
classForest{
varmembers=listOf<Sneil>()
funSneil.reproduce():Sneil=Sneil().also{
it.name=name
it.type=type
members+=it
}
}
TherunandwithfunctionTherunandwithfunctionsthatarebothacceptinglambdaliteralwithreceiverasanargument,andreturningitsresult.Thedifferencebetweenthemisthatrunisacceptingareceiver,whilethewithfunctionisnotanextensionfunctionandittakestheobjectweareoperatinginasparameter.Bothfunctionscanbeusedasanalternativetotheapplyfunction,whenwearesettingupanobject:
valbutton=findViewById(R.id.button)asButton
button.apply{
text="Clickme"
isVisible=true
setOnClickListener{/*...*/}
}
button.run{
text="Clickme"
isVisible=true
setOnClickListener{/*...*/}
}
with(button){
text="Clickme"
isVisible=true
setOnClickListener{/*...*/}
}
Thedifferencebetweenapply,run,andwithisthatapplyisreturningareceiverobject,whilerunandwitharereturningtheresultoffunctionliteral.Althoughwhenweneedanyofthese,weshouldchoosethefunctionthatisreturningit.Itisdebatablewhichshouldbeusedwhenwedonotneedanyreturnedvalue.Mostoften,itissuggestedtousetherunorwithfunctionthan,becausealsoismoreoftenusedinthesituationswhenreturnedvalueisneeded.
Aboutdifferencesbetweentherunandwithfunctions:therunfunctionisusedinsteadofthewithfunction,whenavalueisnullable,becausethenwecanuseasafecallornot-nullassertion:
valbutton=findViewById(R.id.button)as?Button
button?.run{
text="Clickme"
isVisible=true
setOnClickListener{/*...*/}
}
Thewithfunctionispreferredoverrunwhenanexpressionisshort:
valbutton=findViewById(R.id.button)asButton
with(button){
text="Clickme"
isVisible=true
setOnClickListener{/*...*/}
}
Ontheotherhand,runispreferredoverwithwhenanexpressionislong:
itemAdapter.holder.button.run{
text="Clickme"
isVisible=true
setOnClickListener{/*...*/}
}
ThetofunctionInfixfunctionswereintroducedinChapter4,ClassesandObjects,buttheycanbedefinednotonlyasmemberclasses,butalsoasextensionfunctions.Itmakesitpossibletocreateaninfixextensionfunctiontoanyobject.Oneofthesekindsofextensionfunctionsisto,whichwasbrieflydescribedinChapter2,LayingaFoundation.Nowwehavetheknowledgeneededtounderstanditsimplementation.Thisishowtoisdefined:
infixfun<A,B>A.to(that:B):Pair<A,B>=Pair(this,that)
ThismakesitpossibletoplacetobetweenanytwoobjectsandmakethiswayPairwiththem:
println(1to2==Pair(1,2))//Prints:true
Notethatthefactthatwecanmakeinfixextensionfunctionsmakesusallowedtodefineinfixfunctionsasanextensiontoanytype.Hereisanexample:
infixfun<T>List<T>.intersection(other:List<T>)
=filter{itinother}
listOf(1,2,3)intersectionlistOf(2,3,4)//[2,3]
Domain-specificlanguageFeaturessuchaslambdaliteralwithreceiverandmemberextensionfunctionsaremakingitpossibletodefinetype-safebuilders,thatareknownfromGroovy.Themostwell-knownAndroidexampleisGradleconfiguration,build.gradle,whichiscurrentlywritteninGroovy.ThesekindsofbuildersareagoodalternativetoXML,HTML,orconfigurationfiles.TheadvantageofKotlinusageinsteadisthatwecanmakesuchconfigurationsfullytype-safeandprovideabetterIDE.SuchbuildersareoneexampleofKotlindomain-specificlanguage(DSL).
ThemostpopularKotlinDSLpatterninAndroidistheimplementationofoptionalcallbackclasses.Itisusedtosolveaproblemwithalackoffunctionalsupporttocallbackinterfaceswithmultiplemethods.Classically,implementationwouldrequireobject-expressionusage,likeinfollowingexample:
searchView.addTextChangedListener(object:TextWatcher{
overridefunbeforeTextChanged(s:CharSequence,start:Int,count:Int,after:Int){}
overridefunonTextChanged(s:CharSequence,start:Int,before:Int,count:Int){
presenter.onSearchChanged(s.toString())
}
overridefunafterTextChanged(s:Editable){}
})
Themainproblemswithsuchimplementationareasfollows:
WeneedtoimplementallmethodspresentininterfaceFunctionstructureneedstobeimplementedforeachmethodWeneedtouseobjectexpression
Let'sdefinethefollowingclass,thatiskeepingcallbacksasmutableproperties:
classTextWatcherConfig:TextWatcher{
privatevarbeforeTextChangedCallback:(BeforeTextChangedFunction)?=null//1
privatevaronTextChangedCallback:(OnTextChangedFunction)?=null//1
privatevarafterTextChangedCallback:(AfterTextChangedFunction)?=null//1
funbeforeTextChanged(callback:BeforeTextChangedFunction){//2
beforeTextChangedCallback=callback
}
funonTextChanged(callback:OnTextChangedFunction){//2
onTextChangedCallback=callback
}
funafterTextChanged(callback:AfterTextChangedFunction){//2
afterTextChangedCallback=callback
}
overridefunbeforeTextChanged(s:CharSequence?,start:Int,count:Int,
after:Int){//3
beforeTextChangedCallback?.invoke(s?.toString(),start,count,after)//4
}
overridefunonTextChanged(s:CharSequence?,start:Int,before:
Int,count:Int){//3
onTextChangedCallback?.invoke(s?.toString(),start,before,count)//4
}
overridefunafterTextChanged(s:Editable?){//3
afterTextChangedCallback?.invoke(s)
}
}
privatetypealiasBeforeTextChangedFunction=
(text:String?,start:Int,count:Int,after:Int)->Unit
privatetypealiasOnTextChangedFunction=
(text:String?,start:Int,before:Int,count:Int)->Unit
privatetypealiasAfterTextChangedFunction=
(s:Editable?)->Unit
1. Callbacks,whichareusedwhenanyoftheoverriddenfunctionsiscalled.2. Functionsusedtosetnewcallbacks.Theirnamescorrespondstohandler
functionnames,buttheyincludecallbackasaparameter.3. Eacheventhandlerfunctionsarejustinvokingcallbackifitexists.4. Tosimplifyusage,wealsochangedtypes,CharSequencepresentintheoriginal
methodswaschangedtoString.
Nowallweneedisanextensionfunctionthatwillsimplifycallbackconfiguration.ItsnamecannotbethesameasanynameofTextView,butallweneedtodoisasmallmodification:
funTextView.addOnTextChangedListener(config:TextWatcherConfig.()->Unit){
valtextWatcher=TextWatcherConfig()
textWatcher.config()
addTextChangedListener(textWatcher)
}
Withsuchdefinitions,wecandefinecallbacksweneedthisway:
searchView.addOnTextChangedListener{
onTextChanged{text,start,before,count->
presenter.onSearchChanged(text)
}
}
Weuseunderscoretohideunusedparameters,toimproveourimplementation:
searchView.addOnTextChangedListener{
onTextChanged{text,_,_,_->
presenter.onSearchChanged(text)
}
}
NowtwoothercallbacksbeforeTextChangedandafterTextChangedareignored,butwecanstilladdotherimplementations:
searchView.addOnTextChangedListener{
beforeTextChanged{_,_,_,_->
Log.i(TAG,"beforeTextChangedinvoked")
}
onTextChanged{text,_,_,_->
presenter.onSearchChanged(text)
}
afterTextChanged{
Log.i(TAG,"beforeTextChangedinvoked")
}
}
Alistenerdefinedthiswayhasthefollowingproperties:
ItisshorterthanobjectexpressionimplementationItincludesdefaultfunctionsimplementationsItallowsustohideunusedparameters
WhileinAndroidSDKtherearemultiplelistenerswithmorethanonehandler,DSLimplementationofoptionalcallbackclassesisreallypopularinAndroidprojects.Similarimplementationscanbealsofoundinlibraries,suchasthealready-mentionedAnko.
AnotherexampleisDSL,whichwillbeusedtodefinelayoutstructurewithoutusingXMLlayoutfiles.WewilldefineafunctiontoaddandconfigureLinearLayoutandTextViewanduseittodefineasimpleview:
funContext.linearLayout(init:LinearLayout.()->Unit):LinearLayout{
vallayout=LinearLayout(this)
layout.layoutParams=LayoutParams(WRAP_CONTENT,WRAP_CONTENT)
layout.init()
returnlayout
}
funViewGroup.linearLayout(init:LinearLayout.()->Unit):LinearLayout{
vallayout=LinearLayout(context)
layout.layoutParams=LayoutParams(WRAP_CONTENT,WRAP_CONTENT)
layout.init()
addView(layout)
returnlayout
}
funViewGroup.textView(init:TextView.()->Unit):TextView{
vallayout=TextView(context)
layout.layoutParams=LayoutParams(WRAP_CONTENT,WRAP_CONTENT)
layout.init()
addView(layout)
returnlayout
}
//Usage
classMainActivity:AppCompatActivity(){
overridefunonCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
valview=linearLayout{
orientation=LinearLayout.VERTICAL
linearLayout{
orientation=LinearLayout.HORIZONTAL
textView{text="A"}
textView{text="B"}
}
linearLayout{
orientation=LinearLayout.HORIZONTAL
textView{text="C"}
textView{text="D"}
}
}
setContentView(view)
}
}
WecanalsodefineourcustomDSLfromscratch.Let'smakeasimpleDSLthatdefinesalistofarticles.Weknowthateacharticleshouldbedefinedinadifferentcategory,andthatarticlehasitsname,URL,andtags.Whatwewouldliketoachieveisthefollowingdefinition:
category("Kotlin"){
post{
name="Awesomedelegates"
url="SomeUrl.com"
}
post{
name="Awesomeextensions"
url="SomeUrl.com"
}
}
category("Android"){
post{
name="Awesomeapp"
url="SomeUrl.com"
tags=listOf("Kotlin","GoogleLogin")
}
}
ThesimplestobjecthereisthePostclass.Itisholdingpostpropertiesandallowsthemtobechanged:
classPost{
varname:String=""
varurl:String=""
vartags:List<String>=listOf()
}
Next,weneedtodefineaclassthatwillholdthecategory.Itneedstostorealistofpostsanditalsoneedstocontainitsname.Theremustbealsoadefinedfunctionthatwillallowsimplepostaddition.ThisfunctionneedstocontainafunctionparameterinwhichPostisthereceivertype.Hereisthedefinition:
classPostCategory(valname:String){
varposts:List<Post>=listOf()
funpost(init:Post.()->Unit){
valpost=Post()
post.init()
posts+=post
}
}
Also,weneedaclassthatwillholdalistofcategoriesandallowsimplecategorydefinition:
classPostList{
varcategories:List<PostCategory>=listOf()
funcategory(name:String,init:PostCategory.()->Unit){
valcategory=PostCategory(name)
category.init()
categories+=category
}
}
AllweneednowisthedefinePostsfunction,whosedefinitionmightbethefollowing:
fundefinePosts(init:PostList.()->Unit):PostList{
valpostList=PostList()
postList.init()
returnpostList
}
Andthat'sallweneed.Nowwecandefineobjectstructurebyasimple,type-safebuilder:
valpostList=definePosts{
category("Kotlin"){
post{
name="Awesomedelegates"
url="SomeUrl.com"
}
post{
name="Awesomeextensions"
url="SomeUrl.com"
}
}
category("Android"){
post{
name="Awesomeapp"
url="SomeUrl.com"
tags=listOf("Kotlin","GoogleLogin")
}
}
}
DSLisareallypowerfulconceptthatismoreandmoreusedaroundtheKotlincommunity.Itisalreadypossible,thankstolibraries,touseKotlinDSLtofullyreplacethefollowing:
Androidlayoutfiles(Anko)GradleconfigurationfilesHTMLfiles(kotlinx.html)JSONfiles(Kotson)
Andlotsofotherconfigurationfiles.Let'slookatsomeexamplelibrarythatisdefiningKotlinDSLtoprovidetype-safebuilders.
AnkoAnkoisalibrarythatprovidesaDSLtodefineAndroidviewswithoutanyXMLlayouts.Thisisprettysimilartoexampleswe'vealreadyseen,butAnkomadeitpossibletofullyremoveXMLlayoutfilesfromaproject.HereisanexampleviewwritteninAnkoDSL:
verticalLayout{
valname=editText()
button("SayHello"){
onClick{toast("Hello,${name.text}!")}
}
}
Andhereistheresult:
Source:https://github.com/Kotlin/anko
ItisalsopossibletodefinemuchmorecomplexlayoutsusingAnkoDSL.TheseviewscanbeplacedeitheronacustomclassthatservesasavieworevendirectlyinsidetheonCreatemethod:
overridefunonCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
verticalLayout{
padding=dip(30)
editText{
hint="Name"
textSize=24f
}
editText{
hint="Password"
textSize=24f
}
button("Login"){
textSize=26f
}
}
}
Tolearnmoreaboutthisexample,youcanvisitAnkoWikiathttps://g
ithub.com/Kotlin/anko/wiki/Anko-Layouts.
ItisstilldebatableifDSLlayoutdefinitionaregoingtoreplaceXMLdefinitions.Atthetimeofwriting,itisnotsopopulartodefineviewsthisway,becauseofalackofsupportfromGoogle,butwhileGoogleannouncedthattheyaregoingtosupportKotlin,soitispossiblethatthisideawillbecomemorepopularandDSL-basedlayoutswillbemoresupportedandmaybeevenuniversalsomeday.
SummaryInthischapter,wediscussedKotlinextensionfunctionsandproperties,bothdefinedtop-levelandastypemember.We'veseenhowKotlinstandardlibraryextensionfunctionscanbeusedtosimplifycollectionprocessingandperformvariousoperations.Wehavealsodescribedfunctiontypewithreceivertogetherwithfunctionliteralswithreceiver.Also,we'veseenafewimportantgenericfunctionsfromstandardlibrary,whichareusingextensions:let,apply,also,with,run,andto.Finally,we'veseenhowDSLcanbedefinedinKotlin,andwhereitisuseful.
Inthenextchapter,thereisgoingtobepresentedthenextfeaturethatwasnotpresentintheJavaworldwhileitisgivingreallybigpossibilitiesinKotlindevelopment--classandpropertydelegates.
DelegatesKotlintakesdesignpatternsprettyseriously.Previously,we'veseenhowtheusageoftheSingletonpatternwassimplifiedbyobjectdeclarations,andhowusageoftheObservatorpatternwastrivializedthankstohigherorderfunctionsandfunctionaltype.Also,Kotlinsimplifiedtheusageofmostfunctionalpatterns,thankstolambdaexpressionsandfunctionaltypes.Inthischapter,wewillseehowusageofDelegationandDecoratorpatternsweresimplifiedthankstoclassdelegation.Wewillalsoseeafeature,whichisprettynewintheprogrammingworld--propertydelegation--andhowitisusedtomakeKotlinpropertiesmuchmorepowerful.
Inthischapter,wewillcoverthefollowingtopics:
DelegationpatternClassdelegationDecoratorpatternPropertydelegationPropertydelegatesfromthestandardlibraryCreationofacustompropertydelegate
ClassdelegationKotlinhasafeaturecalledclassdelegation.Itisareallyinconspicuousfeaturethathasmanypracticalapplications.Itisworthtonotice,thatitisstronglyconnectedwithtwodesignpatterns:DelegationPatternandDecoratorPattern.Wewilldiscussthosepatternsinmoredetailinupcomingsections.DelegationandDecoratorpatternareknownformanyyears,butinJavatheirimplementationrequiredalotofboilerplatecode.Kotlinisoneofthefirstlanguagesthatprovidednativesupportforthosepatternsreducingboilerplatecodetominimum.
DelegationpatternInobject-orientedprogramming,Delegationpatternisadesignpattern,whichisanalternativetoinheritance.Delegationmeansthattheobjecthandlesarequestbydelegatingittoanotherobject(delegate),insteadofextendingtheclass.
TosupportthepolymorphicbehaviorknownfromJava,bothobjectsshouldimplementthesameinterfacethatholdsalldelegatedmethodsandproperties.Asimpleexampleofthedelegationpatternisthefollowing:
interfacePlayer{//1
funplayGame()
}
classRpgGamePlayer(valenemy:String):Player{
overridefunplayGame(){
println("Killing$enemy")
}
}
classWitcherPlayer(enemy:String):Player{
valplayer=RpgGamePlayer(enemy)//2
overridefunplayGame(){
player.playGame()//3
}
}
//Usage
RpgGamePlayer("monsters").playGame()//Prints:Killingmonsters
WitcherPlayer("monsters").playGame()//Prints:Killingmonsters
1. Whenwearetalkingaboutclassdelegation,thereneedstobeaninterfacethatdefineswhatmethodsaredelegated.
2. Objectthatwearedelegatingto(delegate).3. AllmethodsinsidetheWitcherPlayerclassshouldcallcorrespondingmethods
onthedelegateobject(player).
ThisiscalleddelegationbecausetheWitcherPlayerclassisdelegatingmethodsdefinedinthePlayerinterfacetoaninstanceoftypeRpgGamePlayer(player).Similarresultcouldbereachedbyusinginheritanceinsteadofdelegation.Itwouldlookasfollows:
classWitcherPlayer():RpgGamePlayer()
Atfirstglance,thesetwoapproachesmightlooksimilar,butdelegationandinheritancehavealotofdifferences.Ononehand,inheritanceismuchmorepopularandmorecommoninuse.ItisoftenusedinJava,andisconnectedtomultipleOOPpatterns.Ontheotherhand,therearesourcesthatstronglystandbehinddelegation.Forexample,theinfluentialbookDesignPatterns,bytheGangofFour,containstheprinciple:favorobjectcompositionoverclassinheritance.Also,thepopularbookEffectiveJava,includestherule:favorcompositionoverinheritance(Item6).Bothofthemstronglysupportthedelegationpattern.Herearesomebasicargumentsthatstandbehindtheusageofthedelegationpatterninsteadofinheritance:
Oftenclassesarenotdesignedforinheritance.Whenweoverridemethods,wearenotawareofunderlyingassumptionsaboutclassinternalbehavior(whenmethodsarecalled,howthosecallsaffecttheobjects,states,andsoon).Forexample,whenweoverridemethod,wemightnotbeawarethatitisusedbyothermethods,sooverriddenmethodsmaybecalledunexpectedlybyasuperclass.Evenifwecheckwhenthemethodiscalled,thisbehaviorcouldchangeinanewversionoftheclass(forexample,ifweextendclassfromanexternallibrary)andthusbreakoursubclass'behavior.Averysmallamountofclassesareproperlydesignedanddocumentedforinheritance,butnearlyallnonabstractclassesaredesignedforusage(thisincludesdelegation).InJava,itispossibletodelegateaclasstomultipleclasses,butinheritonlyfromone.Byinterface,wearespecifyingwhichmethodsandpropertieswewanttodelegate.Thisiscompatiblewiththeinterfacesegregationprinciple(fromSOLID)--weshouldn'texposeunnecessarymethodstotheclient.Someclassesarefinal,sowecanonlydelegatetothem.Infact,allclassesthatarenotdesignedforinheritanceshouldbefinal.Kotlindesignerswereawareofit,andtheymadeallclassesinKotlintobefinalbydefault.Makingaclassfinalandprovidingproperinterfaceisgoodpracticeforpubliclibraries.Wecanchangetheimplementationofaclasswithoutworryingthatitwillaffectlibraryusers(aslongasthebehaviorwillbethesamefromaninterfacepointofview).Itmakesthemimpossibletoinheritfrom,buttheyarestillgreatcandidatestodelegateto.
Moreinformationonhowclassesshouldbedesignedtosupport
inheritanceandwhendelegationshouldbeusedcanbefoundinthebookEffectiveJava,inItem16:Favorcompositionoverinheritance.
Ofcourse,therearealsodisadvantagesofusingdelegationinsteadofinheritance.Herearethemainproblems:
WeneedtocreateinterfacesthatspecifywhichmethodsshouldbedelegatedWedon'thaveaccesstoprotectedmethodsandproperties
InJava,therewasonemorestrongargumentforusinginheritance:itwasmucheasiertoimplement.EvenwhilecomparingcodefromourWitcherPlayerexample,wecanseethatdelegationneededalotofextracode:
classWitcherPlayer(enemy:String):Player{
valplayer=RpgGamePlayer(enemy)
overridefunplayGame(){
player.playGame()
}
}
classWitcherPlayer():RpgGamePlayer()
Thisisespeciallyproblematicwhenwearedealingwithinterfaceswithmultiplemethods.Fortunately,modernlanguagesvaluetheusageoftheDelegationpattern,andmanyofthemhavenativeclassdelegationsupport.ThereisstrongsupportfortheDelegationpatterninSwiftandGroovy,andthereisalsosupportthroughothermechanismsinRuby,Python,JavaScript,andSmalltalk.Kotlinalsostronglysupportsclassdelegation,andmakesusageofthispatternreallysimple,andusingnearlyzero-boilerplatecode.TheWitcherPlayerclassfromtheexamplecouldbeimplementedinthiswayinKotlin:
classWitcherPlayer(enemy:String):PlayerbyRpgGamePlayer(enemy){}
Usingthebykeyword,weareinformingthecompilertodelegateallmethodsdefinedinthePlayerinterfacefromWitcherPlayertoRpgGamePlayer.AninstanceofRpgGamePlayeriscreatedduringtheWitcherPlayerconstruction.Insimplerwords:WitcherPlayerisdelegatingmethodsdefinedinthePlayerinterfacetoanewRpgGamePlayerobject.
Whatisreallyhappeninghereisthatduringcompilation,theKotlincompileris
generatingnotimplementedmethodsfromPlayerinWitcherPlayerandfillingthemwithcallstoanRpgGamePlayerinstance(thesamewayasthatweimplementedtheminthefirstexample).Thebigimprovementisthatwedon'tneedtoimplementthosemethodsourselves.Alsonotethatifasignatureofadelegatedmethodchanges,thenwedon'tneedtochangeallobjectsthataredelegatedtoit,sotheclassiseasiertomaintain.
Thereisanotherwaytocreateandholdaninstanceofthedelegate.Itcanbeprovidedbyaconstructor,likeinthisexample:
classWitcherPlayer(player:Player):Playerbyplayer
Wecanalsodelegatetoapropertydefinedintheconstructor:
classWitcherPlayer(valplayer:Player):Playerbyplayer
Finally,wecandelegatetoanypropertyaccessibleduringclassdeclaration:
vald=RpgGamePlayer(10)
classWitcherPlayer(a:Player):Playerbyd
Inaddition,oneobjectcanhavemultipledifferentdelegates:
interfacePlayer{
funplayGame()
}
interfaceGameMaker{//1
fundevelopGame()
}
classWitcherPlayer(valenemy:String):Player{
overridefunplayGame(){
print("Killin$enemy!")
}
}
classWitcherCreator(valgameName:String):GameMaker{
overridefundevelopGame(){
println("Makin$gameName!")
}
}
classWitcherPassionate:
PlayerbyWitcherPlayer("monsters"),
GameMakerbyWitcherCreator("Witcher3"){
funfulfillYourDestiny(){
playGame()
developGame()
}
}
//Usage
WitcherPassionate().fulfillYourDestiny()//Killinmonsters!MakinWitcher3!
1. TheWitcherPlayerclassisdelegatingthePlayerinterfacetoanewRpgGamePlayerobject,GameMakertoanewWitcherCreatorobject,andalsoincludesthefunctionfulfillYourDestinythatisusingfunctionsfrombothdelegates.NotethatbothWitcherPlayerandWitcherCreatorarenottaggedasopen,andwithoutthis,theycannotbeextended.Theycanbedelegated,though.
Withsuchlanguagesupport,theDelegationpatternismuchmoreattractivethaninheritance.Whilethispatternhasbothadvantagesanddisadvantages,itisgoodtoknowwhenitshouldbeused.Themaincaseswheredelegatesshouldbeusedareasfollows:
WhenyoursubclassviolatestheLiskovsubstitutionprinciple;forexample,whenwearedealingwithsituationswhereinheritancewasimplementedonlytoreusecodeofthesuperclass,butitisnotreallyactinglikeit.Whenthesubclassusesonlyaportionofthemethodsofthesuperclass.Inthiscase,itisonlyamatteroftimebeforesomeonecallsasuperclassmethodthattheywerenotsupposedtocall.Usingdelegation,wereuseonlymethodswechoose(definedintheinterface).Whenwecannotorweshouldnotinherit,because:
TheclassisfinalItisnotaccessibleandusedfrombehindinterfaceItisjustnotdesignedforinheritance
NotethatwhileclassesinKotlinarefinalbydefault,mostofthemwillbeleftfinal.Ifthoseclassesareplacedinalibrarythenmostlikelywewon'tbeabletochangeoropentheclass.Delegationwillbetheonlyoption,tomakeclasswithdifferentbehavior.
TheLiskovsubstitutionprincipleisaconceptinOOPstatingthatallsubclassesshouldactliketheirsuperclasses.Ineasierwords,Ifunittestsarepassingforsomeclass,theyshouldbepassingforitssubclassestoo.ThisprinciplehasbeenpopularizedbyRobertC.Martin,whoplaceditinhissetofmostimportantOOPrulesanddescribeditinthepopularbookCleanCode.
ThebookEffectiveJavastatesthat"inheritanceisappropriateonlyin
circumstanceswhereasubclassreallyisasubtypeofthesuperclass."Inotherwords,classBshouldextendaclassonlyifanis-arelationshipexistsbetweenthetwoclasses.IfyouaretemptedtohaveclassBextendclassA,askyourselfIseveryBreallyanA?Inthenextpart,thebooksuggeststhatineveryothercasethereshouldbecompositionused(whichmostcommonimplementationisdelegation).
ItisalsoworthnotethatCocoa(theUIframeworkfromAppleforbuildingsoftwareprogramstorunoniOS)veryoftenusedelegatesinsteadofinheritance.Thispatternisbecomingmoreandmorepopular,andinKotlinitishighlysupported.
DecoratorpatternAnothercommoncasewheretheKotlinclassdelegationisreallyusefuliswhenweareimplementingaDecoratorpattern.ADecoratorpattern(alsoknownasWrapperpattern)isadesignpatternthatmakesitpossibletoaddabehaviortoanexistingclasswithoutusinginheritance.Incontrasttoextensionswherewecanaddanewbehaviorwithoutmodifyinganobject,wearecreatingaconcreteobjectwithadifferentbehavior.ADecoratorpatternusesDelegation,butinaveryspecificway--delegateisprovidedfromoutsideoftheclass.TheclassicstructureispresentedinthefollowingUMLdiagram:
UMLdiagramofclassicimplementationoftheDecoratorpattern.Source:http://upload.wikimedia.org
TheDecoratorcontainstheobjectsthatitisdecoratingwhileitisimplementingthesameinterface.
ThemostpopularexampleofdecoratorusagefromtheJavaworldisInputStream.TherearedifferentkindsoftypesthatareextendingInputStream,andalotofdecoratorsthatcanbeusedtoaddfunctionalitiestothem.Thisdecoratorcanbe
usedtoaddbuffering,getcontentofazippedfile,orconvertfilecontenttoaJavaobject.Let'slookattheexamplewheremultipledecoratorsareusedtoreadazippedJavaobject:
//Java
FileInputStreamfis=newFileInputStream("/someFile.gz");//1
BufferedInputStreambis=newBufferedInputStream(fis);//2
GzipInputStreamgis=newGzipInputStream(bis);//3
ObjectInputStreamois=newObjectInputStream(gis);//4
SomeObjectsomeObject=(SomeObject)ois.readObject();//5
1. Createasimplestreamforreadingafile.2. Makeanewstreamthatcontainsbuffering.3. Makeanewstreamthatcontainsfunctionalityofthereadingcompressed
dataintheGZIPfileformat.4. Makeanewstreamthataddsfunctionalitythatdeserializesprimitivedata
andobjectspreviouslywrittenusinganObjectOutputStream.5. StreamisusedinthereadObjectmethodofObjectInputStream,butallobjectsin
thisexampleareimplementingInputStream(whatmakesitpossibletopackitthisway)andcanbereadbythemethodsspecifiedbythisinterface.
Notethatthispatternisalsosimilartoinheritance,butwecandecidewhatdecoratorswewanttouseandinwhatorder.Thisismuchmoreflexibleandgivesmorepossibilitiesduringusage.SomepeoplearguethatInputStreamusagewouldbebetterifdesignerswouldmakeonebigclasswithalldesignedfunctionalities,andthenusemethodstoturnsomeofthemonoroff.Thisapproachwouldviolatethesingle-responsibilityprincipleandleadtomuchmorecomplicatedandmuchlessexpandablecode.
WhiletheDecoratorpatternisconsideredoneofthebestinpracticaluse,itisrarelyusedinJavaprojects.Thisisbecausetheimplementationisnotsimple.Interfacesoftencontainmultiplemethodsandcreatingadelegationtothemineachdecoratorgenerateslotsofboilerplatecode.ThereisadifferentsituationinKotlin--we'vealreadyseenthatinKotlinclassdelegationisactuallytrivial.Let'slookatsomeclassicexamplesofpracticalclassdelegationusageintheDecoratorpattern.Let'ssupposethatwewanttoaddthefirstpositionasthezeroelementtoseveraldifferentListAdapters.Thisextrapositionhassomespecialproperties.Wecouldn'timplementthisusinginheritancebecausetheseListAdaptersfordifferentlistsareofdifferenttypes(whichisthestandardsituation).Inthiscase,wecaneitherchangethebehaviorsofeachclass(DRY
rule)orwecancreateadecorator.Hereistheshortcodeofthisdecorator:
classZeroElementListDecorator(valarrayAdapter:ListAdapter):
ListAdapterbyarrayAdapter{
overridefungetCount():Int=arrayAdapter.count+1
overridefungetItem(position:Int):Any?=when{
position==0->null
else->arrayAdapter.getItem(position-1)
}
overridefungetView(position:Int,convertView:View?,parent:
ViewGroup):View=when{
position==0->parent.context.inflator
.inflate(R.layout.null_element_layout,parent,false)
else->arrayAdapter.getView(position-1,convertView,parent)
}
}
overridefungetItemId(position:Int):Long=when{
position==0->0
else->arrayAdapter.getItemId(position-1)
}
WeusedaninflatorextensionpropertyofContexthere,whichisoftenincludedinKotlinAndroidprojectsandshouldbeknownfromChapter7,ExtensionFunctionsandProperties:
valContext.inflater:LayoutInflater
get()=LayoutInflater.from(this)
TheZeroElementListDecoratorclassdefinedthiswayalwaysaddsafirstelementwithstaticview.Herewecanseeasimpleexampleofitsuse:
valarrayList=findViewById(R.id.list)asListView
vallist=listOf("A","B","C")
valarrayAdapter=ArrayAdapter(this,
android.R.layout.simple_list_item_1,list)
arrayList.adapter=ZeroElementListDecorator(arrayAdapter)
InZeroElementListDecoratoritmightlookcomplicatedthatweneededtooverridefourmethods,butinfact,thereareeightmoreofthemandwedidn'thavetooverridethem,thankstoKotlin'sclassdelegation.WecanseethatKotlinclassdelegationismakingtheimplementationoftheDecoratorpatternmucheasier.
TheDecoratorpatternisreallysimpletoimplementandisprettyintuitive.Itcanbeusedinlotsofdifferentcasestoextendaclasswithextrafunctionality.Itisreallysafeandoftenreferredasagoodpractice.Theseexamplesarejustsomeofthepossibilitiesprovidedbyclassdelegation.Iamsurethatthereaderwillfindmoreusecaseswithpresentedpatternsanduseclassdelegationtomakethe
projectmoreclean,safe,andconcise.
PropertydelegationKotlinallowsnotonlyclassdelegation,butalsopropertydelegation.Inthissection,wearegoingtofindoutwhatdelegatedpropertiesare,reviewpropertydelegatesfromKotlinstandardlibrary,andlearnhowtocreateandusecustompropertydelegates.
Whataredelegatedproperties?Let'sstartwithexplanationofwhatpropertydelegatesare.Hereisanexampleoftheuseofpropertydelegation:
classUser(valname:String,valsurname:String)
varuser:UserbyUserDelegate()//1
println(user.name)
user=User("Marcin","Moskala")
1. WearedelegatinguserpropertytoaninstanceofUserDelegate(whichiscreatedbytheconstructor).
Propertydelegationissimilartoclassdelegation.Wedelegatetoanobjectusingthesamekeyword(by).Eachcalltoaproperty(set/get)willbedelegatedtoanotherobject(UserDelegate).Thiswaywecanreusethesamebehaviorformultipleproperties,forexample,settingapropertyvalueonlywhensomecriteriaaremet,oraddinglogentrywhenapropertyisaccessed/updated.
Weknowthatapropertydoesn'treallyneedabackingfield.Itmightbedefinedjustbygetter(read-only)oragetter/setter(read-write).Underthehood,propertydelegatesarejusttranslatedtocorrespondingmethodcalls(setValue/getValue).Theprecedingexamplewillbecompiledtosuchcode:
varp$delegate=UserDelegate()
varuser:User
get()=p$delegate.getValue(this,::user)
set(value){
p$delegate.setValue(this,::user,value)
}
Theexampleisshowingthatbyusingthebykeyword,wearedelegatingthesetterandgettercallstodelegate.ThatiswhyanyobjectthathasthegetValueandsetValuefunctionswithcorrectparameters(itwillbedescribedlater)canbeusedasadelegate(forread-onlypropertiesgetValueisenough,becauseonlythegetterisneeded).Itisimportantthatallthatclassneedstobeabletoserveasapropertydelegateistohavethesetwomethods.Nointerfaceisneeded.HereisanexampleimplementationofUserDelegate:
classUserDelegate{
operatorfungetValue(thisRef:Any?,property:KProperty<*>):
User=readUserFromFile()
operatorfunsetValue(thisRef:Any?,property:KProperty<*>,
user:User){
saveUserToFile(user)
}
//...
}
ThesetValueandgetValuemethodsareusedtosetandgetvalueofaproperty(propertysettercallisdelegatedtoasetValuemethod,andpropertygetterisdelegatingvaluetothegetValuemethod).Bothfunctionsneedtobemarkedwiththeoperatorkeyword.Theyhavesomespecialsetofparametersthatdeterminewhereandtowhichpropertythedelegatecanserve.Ifapropertyisread-only,thenanobjectonlyneedstohaveagetValuemethodtobeabletoserveasitsdelegate:
classUserDelegate{
operatorfungetValue(thisRef:Any?,property:KProperty<*>):
User=readUserFromFile()
}
ThetypereturnedbythegetValuemethodandthetypeofpropertythattheuserdefinedinthesetValuemethoddeterminestypeofdelegatedproperty.
TypeofthefirstparameterofboththegetValueandsetValuefunctions(thisRef)iscontainsthereferencetocontextinwhichadelegateisused.Itcanbeusedtorestricttypesthatdelegatecanbeusedfor.Forexample,wecandefinedelegatethatmightbeusedonlyinsideanActivityclassinthefollowingway:
classUserDelegate{
operatorfungetValue(thisRef:Activity,property:KProperty<*>):
User=thisRef.intent
.getParcelableExtra("com.example.UserKey")
}
Aswe'veseen,therewillbeareferencetothisprovidedinallcontextswhereitisavailable.Onlyinsideextensionfunctionoronextensionpropertythereisnullplacedinstead.Referencetothisisusedtogetsomedatafromcontext.IfwewouldtypeittoActivity,thenwewouldbeabletousethisdelegateonlyinsideActivity(inanycontextwherethisisofthetypeActivity).
Also,ifwewanttoforcethedelegatetobeusedonlyonthetop-level,wecan
thenspecifythefirstparameter(thisRef)typetoNothing?,becausetheonlypossiblevalueofthistypeisnull.
Anotherparameterinthesemethodsisproperty.Itcontainsareferencetoadelegatedproperty,whichcontainsitsmetadata(propertyname,type,andsoon).
Propertydelegationcanbeusedforpropertiesdefinedinanycontext(top-levelproperties,memberproperties,localvariables,andsoon):
varabySomeDelegate()//1
funsomeTopLevelFun(){
varbbySomeDelegate()//2
}
classSomeClass(){
varcbySomeDelegate()//3
funsomeMethod(){
valdbySomeDelegate()//4
}
}
1. Top-levelpropertywithdelegate2. Localvariable(insidetop-levelfunction)withdelegate3. Memberpropertywithdelegate4. Localvariable(insidemethod)withdelegate
Inthenextfewsections,wewilldescribedelegatesfromtheKotlinstandardlibrary.Theyareimportantnotonlybecausetheyareoftenuseful,butalsobecausetheyaregoodexamplesofhowpropertydelegationcanbeused.
PredefineddelegatesTheKotlinstandardlibrarycontainssomepropertydelegatesthatareveryhandy.Let'sdiscusshowtheycanbeusedinreal-lifeprojects.
ThelazyfunctionSometimesweneedtoinitializeanobject,butwewanttomakesurethattheobjectwillbeinitializedonlyonce,whenitisusedforthefirsttime.InJava,wecouldsolvethisprobleminthefollowingway:
privatevar_someProperty:SomeType?=null
privatevalsomePropertyLock=Any()
valsomeProperty:SomeType
get(){
synchronized(somePropertyLock){
if(_someProperty==null){
_someProperty=SomeType()
}
return_someProperty!!
}
}
ThisconstructionisapopularpatterninJavadevelopment.Kotlinallowsustosolvethisprobleminamuchsimplerwaybyprovidingthelazydelegate.Itisthemostcommonlyuseddelegate.Itworksonlywithread-onlyproperties(val)anditsusageisasfollows:
valsomePropertybylazy{SomeType()}
Thelazyfunctioninthestandardlibraryfunctionthatisprovidingdelegate:
publicfun<T>lazy(initializer:()->T):
Lazy<T>=SynchronizedLazyImpl(initializer)
Formally,inthisexampleobjectofSynchronizedLazyImpl,itisusedasapropertydelegate.Although,mostoftenitiscalledlazydelegatefromitscorrespondingfunctionname.Thesamewayotherdelegatesarenamedfromthenamesofthefunctionsthatareprovidingthem.
Thelazydelegatealsohasathreadsafetymechanism.Bydefault,delegatesarefullythreadsafe,butwecanchangethisbehaviortomakethisfunctionmoreefficientinsituationswhereweknowthatthereneverwillbemorethanonethreadusingitatthesametime.Tofullyturnoffthread-safetymechanismsweneedtoplacetheenumtypevalueLazyThreadSafetyMode.NONEasafirstargumentofthelazyfunction:
valsomePropertybylazy(LazyThreadSafetyMode.NONE){SomeType()}
Thankstothelazydelegate,theinitializationofthepropertyisdelayeduntilthevalueisneeded.Usageofthelazydelegateprovidesseveralbenefits:
Fasterclassinitializationleadingtofasterapplicationstartuptime,becausevalueinitializationisdelayeduntiltheyareusedforthefirsttimeSomevaluesmayneverbeusedforcertainflow,sotheywillneverbeinitialized--wearesavingresources(memory,processortime,battery)
Anotherbenefitisthatsomeobjectsneedtobecreatedlater,aftertheirclassinstanceiscreated.Forexample,inActivitywecannotaccesstheresourcesbeforelayoutissetusingthesetContentViewmethod,whichistypicallycalledinsidetheonCreatemethod.Iwillpresentitinthisexample.Let'slookattheJavaclasswithviewreferenceelementsfilledintheclassicJavaway:
//Java
publicclassMainActivityextendsActivity{
TextViewquestionLabelView
EditTextanswerLabelView
ButtonconfirmButtonView
@Override
publicvoidonCreate(BundlesavedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
questionLabelView=findViewById<TextView>
(R.id.main_question_label);
answerLabelView=findViewById<EditText>
(R.id.main_answer_label);
confirmButtonView=findViewById<Button>
(R.id.main_button_confirm);
}
}
IfwetranslateitintoKotlin,one-to-one,itwilllookasfollows:
classMainActivity:Activity(){
varquestionLabelView:TextView?=null
varanswerLabelView:TextView?=null
varconfirmButtonView:Button?=null
overridefunonCreate(savedInstanceState:Bundle){
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
questionLabelView=findViewById<TextView>
(R.id.main_question_label)
answerLabelView=findViewById<TextView>
(R.id.main_answer_label)
confirmButtonView=findViewById<Button>
(R.id.main_button_confirm)
}
}
Usingthelazydelegate,wecanimplementthisbehaviorinasimplerway:
classMainActivity:Activity(){
valquestionLabelView:TextViewbylazy
{findViewById(R.id.main_question_label)asTextView}
valanswerLabelView:TextViewbylazy
{findViewById(R.id.main_answer_label)asTextView}
valconfirmButtonView:Buttonbylazy
{findViewById(R.id.main_button_confirm)asButton}
overridefunonCreate(savedInstanceState:Bundle){
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
}
}
Thebenefitsofthisapproachareasfollows:
Thepropertyisdeclaredandinitializedinasingleplace,sothecodeismoreconcise.Thepropertiesarenon-nullableinsteadofnullable.Thispreventslotsofuselessnullabilitychecks.Thepropertiesarereadonlysothankstothatwehaveallbenefitslikethreadssynchronizationorsmartcasts.Thelambdapassedtothelazydelegate(containingfindViewById)willbeexecutedonlywhenthepropertyisaccessedforthefirsttime.Valueswillbetakenlaterthanduringclasscreation.Thiswillspeed-upthestartup.Ifwewon'tusesomeoftheseviews,theirvalueswon'tbetakenatall(findViewByIdisnotreallyanefficientoperationwhentheviewiscomplex).Notusedpropertywillbemarkedbythecompiler.InJavaimplementationitwon't,becausevaluesetwouldbenoticedbythecompilerasusage.
Wecanimprovetheprecedingimplementationbyextractingthecommonbehaviorandconvertingitintoanextensionfunction:
fun<T:View>Activity.bindView(viewId:Int)=lazy{findViewById(viewId)asT}
Then,wecandefinetheviewbindingsinsimplerandmoreconcisecode:
classMainActivity:Activity(){
varquestionLabelView:TextViewbybindView(R.id.main_question_label)//1
varanswerLabelView:TextViewbybindView(R.id.main_answer_label)//1
varconfirmButtonView:ButtonbybindView(R.id.main_button_confirm)//1
overridefunonCreate(savedInstanceState:Bundle){
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
}
}
1. Wedon'tneedtosettypeprovidedtothebindViewfunctionbecauseitisinferredfrompropertytype.
NowwehaveasingledelegatethatcallsfindViewByIdunderthehood,whenweaccessaparticularviewforthefirsttime.Thisisaveryconcisesolution.
Thereisanotherwayofdealingwiththisproblem.ThecurrentpopularoneistheKotlinAndroidExtensionplugin,whichgeneratesauto-bindingtoviewsinActivitiesandFragments.WewilldiscussthepracticalapplicationsinChapter9,MakingyourMarvelGalleryApplication.
Evenwithsuchsupport,therearestillbenefitsfromstayingwithbindings.Oneisexplicitknowledgeofwhatelementsofviewweareusing,andanotheristheseparationbetweenthenameofelementIDandnameofavariableinwhichweholdthiselement.Alsocompilationtimeisfaster.
ThesamemechanismcanbeappliedtosolveotherAndroidrelatedproblems.Forexample,whenwepassanargumenttoActivity.ThestandardJavaimplementationlooksasfollows:
//Java
classSettingsActivityextendsActivity{
finalDoctorDOCTOR_KEY="doctorKey"
finalStringTITLE_KEY="titleKey"
Doctordoctor
Addressaddress
Stringtitle
publicstaticvoidstart(Contextcontext,Doctordoctor,
Stringtitle){
Intentintent=newIntent(context,SettingsActivity.class)
intent.putExtra(DOCTOR_KEY,doctor)
intent.putExtra(TITLE_KEY,title)
context.startActivity(intent)
}
@Override
publicvoidonCreate(BundlesavedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
doctor=getExtras().getParcelable(DOCTOR_KEY)
title=getExtras().getString(TITLE_KEY)
ToastHelper.toast(this,doctor.id)
ToastHelper.toast(this,title)
}
}
WecouldwritethesameimplementationinKotlin,butwecanalsoretrieveparametervalues(getString/getParcerable)togetherwiththevariabledeclaration.Todothis,weneedthefollowingextensionfunctions:
fun<T:Parcelable>Activity.extra(key:String)=lazy
{intent.extras.getParcelable<T>(key)}
funActivity.extraString(key:String)=lazy
{intent.extras.getString(key)}
ThenwecangetextraparametersbyusingextraandextraStringdelegates:
classSettingsActivity:Activity(){
privatevaldoctorbyextra<Doctor>(DOCTOR_KEY)//1
privatevaltitlebyextraString(TITLE_KEY)//1
overridefunonCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.settings_activity)
toast(doctor.id)//2
toast(title)//2
}
companionobject{//3
constvalDOCTOR_KEY="doctorKey"
constvalTITLE_KEY="titleKey"
funstart(context:Context,doctor:Doctor,title:String){//3
ontext.startActivity(getIntent<SettingsActivity>().apply{//4
putExtra(DOCTOR_KEY,doctor)//5
putExtra(TITLE_KEY,title)//5
})
}
}
}
1. WearedefiningpropertieswhichvaluesshouldberetrievedfromActivityargumentsusingcorrespondingkeys.
2. HereweaccesspropertiesfromargumentswithintheonCreatemethod.Whenweaskforproperty(usegetter),thelazydelegatewillgetitsvalue
fromextrasandstoreitforlaterusage.
3. Tomakeastaticmethodtostartactivity,weneedtouseacompanionobject.
4. SettingsActivity::class.javaistheanalogueofJavaclassreferenceSettingsActivity.class.
5. WeareusingmethodsdefinedinChapter7,ExtensionFunctionsandProperties.
WecanalsomakefunctionstoretrieveothertypesthatcanbeheldbyBundle(forexample,Long,Serializable).ThisisaprettynicealternativetotheargumentinjectionlibrariessuchasActivityStarter,whenwewanttokeepareallyfastcompilationtime.Wecanusesimilarfunctionstobindstrings,colors,services,repositories,andotherpartsofmodelandlogic:
fun<T>Activity.bindString(@IdResid:Int):Lazy<T>=
lazy{getString(id)}
fun<T>Activity.bindColour(@IdResid:Int):Lazy<T>=
lazy{getColour(id)}
InActivity,everythingthatisheavyordependsonargumentsshouldbedeclaredusinglazydelegate(orprovidedasynchronously).Alsoweshoulddefineaslazyalltheelementsthatdependonelementsthatneedtobeinitializedlazily.Forexample,presenterdefinition,whichdependsonthedoctorproperty:
valpresenterbylazy{MainPresenter(this,doctor)}
Otherwise,theattempttoconstructaMainPresenterobjectwilltakeplaceduringclasscreationwhenwecannotyetreadvaluesfromtheintentanditwouldn'tbeabletofillthedoctorproperty,andtheapplicationwouldcrash.
IthinkthattheseexamplesareenoughtoconvinceusthatthelazydelegateisreallyusefulinAndroidprojects.Itisalsoagoodpropertydelegatetostartwith,asitissimpleandelegant.
ThenotNullfunctionThenotNulldelegateisthesimpleststandardlibrarydelegate,andthatiswhyitwillbepresentedfirst.Theusageisasfollows:
varsomeProperty:SomeTypebynotNull()
Functionsthatprovidemoststandardlibrarydelegates(includingthenotNullfunction)aredefinedinobjectdelegates.Tousethem,weneedtoeitherrefertothisobject(Delegates.notNull())orimportit(importkotlin.properties.Delegates.notNull).Wewillassumeinexamplesthatthisobjectisimportedsowewillomitreferencetoit.
ThenotNulldelegateallowsustodefineavariableasnon-nullable,thatisinitializedatalatertimeandnotduringtheobjectconstructiontime.Wecandefinevariabletobenon-nullablewithoutprovidingadefaultvalue.ThenotNullfunctionisanalternativetolateinit:
lateinitvarsomeProperty:SomeType
ThenotNulldelegateprovidesnearlythesameeffectaslateinit(onlytheerrormessageisdifferent).Inthecaseoftryingtousethispropertybeforesettingthevaluefirst,itwillthrowanIllegalStateExceptionanditwillterminateanAndroidapplication.Therefore,itshouldbeusedonlywhenweknowthatavaluewillbesetbeforethefirstattemptofusage.
ThedifferencebetweenlateinitandthenotNulldelegateisprettysimple.lateinitisfasterthannotNulldelegatesoitshouldbeusedinsteadofnotNulldelegateasoftenaspossible.Butithasrestrictions,lateinitcannotbeusedforprimitivesorfortop-levelproperties,sointhiscase,notNullisusedinstead.
Let'slookatthenotNulldelegateimplementation.HereisthenotNullfunctionimplementation:
publicfun<T:Any>notNull():ReadWriteProperty<Any?,T>=
NotNullVar()
Aswecansee,notNullisactuallyafunctionreturninganobjectthatisaninstance
ofouractualdelegatehiddenbehindaReadWritePropertyinterface.Let'slookatanactualdelegatedefinition:
privateclassNotNullVar<T:Any>():ReadWriteProperty<Any?,T>{//1
privatevarvalue:T?=null
publicoverridefungetValue(thisRef:Any?,
property:KProperty<*>):T{
returnvalue?:throwIllegalStateException("Property
${property.name}shouldbeinitializedbeforeget.")//2
}
publicoverridefunsetValue(thisRef:Any?,
property:KProperty<*>,value:T){
this.value=value
}
}
1. Classisprivate.ItispossiblebecauseitisprovidedbyfunctionnotNull,whichisreturningitasReadWriteProperty<Any?,T>,whichispublicinterface.
2. Hereweseehowareturnvalueisprovided.Ifitisnullduringusage,thenvaluewasnotsetandthemethodwillthrowanerror.Otherwise,itisreturningthevalue.
Thisdelegateshouldbeprettysimpletounderstand.ThesetValuefunctionsetsthevaluetoanullablefieldandgetValuereturnsthisfieldifitisnotnull,andthrowsanexceptionifitis.Hereisanexampleofthiserror:
varname:StringbyDelegates.notNull()
println(name)
//Error:Propertynameshouldbeinitializedbeforeget.
Thisisareallysimpleexampleofdelegatedpropertiesusage,butalsoagoodintroductiontohowpropertydelegatesworks.Delegatedpropertiesareverypowerfulconstructsthathavemultipleapplications.
TheobservabledelegateAnobservableisthemostusefulstandardlibrarydelegateformutableproperties.Everytimeavalueisset(setValuemethodiscalled),thelambdafunctionfromthedeclarationisinvoked.Asimpleexampleofobservabledelegateisasfollows:
varname:StringbyDelegates.observable("Empty"){
property,oldValue,newValue->//1
println("$oldValue->$newValue")//2
}
//Usage
name="Martin"//3,
Prints:Empty->Martin
name="Igor"//3,
Prints:Martin->Igor
name="Igor"//3,4
Prints:Igor->Igor
1. Theargumentsoflambdafunctionareasfollows:property:Referencetothedelegatedproperty.Hereitisreferencetoname.ThisisthesameaspropertyfromsetValueandgetValue,whichwasdescribed.ItisoftheKPropertytype.Inthiscase(andinmostcases)wecanputtheunderscore("_"sign)insteadwhenitisnotused.oldValue:Thepreviousvalueoftheproperty(beforethechange).newValue:Thenewvalueoftheproperty(afterthechange).
2. Thelambdafunctionwillbeinvokedeachtimeanewvalueissettotheproperty.
3. Whenwesetthenewvalue,thenthevalueisupdated,butatthesametimelambdamethoddeclaredindelegateiscalled.
4. Notethatlambdaisinvokedeachtimesetterisusedanditdoesn'tmatterifanewvalueisequaltoprevious.
Itisparticularlyimportanttorememberthatlambdaiscalledeachtimeanewvalueisset,andnotwhenanobject'sinnerstateischanged.Forexample:
varlist:MutableList<Int>byobservable(mutableListOf())
{_,old,new->
println("Listchangedfrom$oldto$new")
}
//Usage
list.add(1)//1
list=mutableListOf(2,3)
//2,prints:Listchangedfrom[1]to[2,3]
1. Doesnotprintanything,becausewedon'tchangetheproperty(thesetterisnotused).Weonlychangethepropertydefinedinsidethelist,butnottheobjectitself.
2. Herewechangethevalueoflist,sothelambdafunctionfromobservabledelegateiscalledandtextisprinted.
Observabledelegateisveryusefulforimmutabletypes,asopposedtomutableones.Fortunately,allbasictypesinKotlinareimmutablebydefault(List,Map,Set,Int,String).Let'slookatapracticalAndroidexample:
classSomeActivity:Activity(){
varlist:List<String>byDelegates.observable(emptyList()){
prop,old,new->if(old!=new)updateListView(new)
}
//...
}
Everytimewechangethelist,theviewisupdated.NotethatwhileListisimmutable,weneedtousesetterwhenwewanttoapplyanychanges,sowecanbesurethatafterthisoperationthelistwillbeupdated.ItismucheasierthanrememberingtocalltheupdateListViewmethodeverytimethelistchanges.Thispatterncanbeusedwidelyintheprojecttodeclarepropertiesthatareeditingviews.Itchangesthewaytheupdateviewmechanismcanwork.
AnotherproblemthatcanbesolvedusinganobservabledelegateisthatinListAdapterstherewasalwaysaproblemthatnotifyDataSetChangedhadtobecalledeachtimeelementsonthelistwerechanged.InJava,theclassicsolutionwastoencapsulatethislist,andcallnotifyDataSetChangedineachfunctionthatismodifyingit.InKotlin,wecansimplifythisusinganobservablepropertydelegate:
varlist:List<LocalDate>byobservable(list){_,old,new->//1
if(new!=old)notifyDataSetChanged()
}
1. Notethatherelistisimmutable,sothereisnowaytochangeitselementswithoutusingnotifyDataSetChanged.
Theobservabledelegateisusedtodefinebehaviorthatshouldhappenonthe
propertyvaluechange.Itismostfrequentlyusedwhenwehaveoperationsthatshouldbedoneeverytimewechangeaproperty,orwhenwewanttobindapropertyvaluewithavieworsomeothervalues.Butinsidethefunctionwecannotdecideifanewvaluewillbesetornot.Forthis,thevetoabledelegateisusedinstead.
ThevetoabledelegateThevetoablefunctionisastandardlibrarypropertydelegatethatworkssimilarasanobservabledelegate,butwithtwomaindifferences:
ThelambdafromanargumentiscalledbeforeanewvalueissetItallowsthelambdafunctionfromadeclarationtodecideifanewvalueshouldbeacceptedorrejected
Forexample,ifwehaveanassumptionthatthelistmustalwayscontainlargernumberofitemsthantheoldone,thenwewilldefinethefollowingvetoabledelegate:
varlist:List<String>byDelegates.vetoable(emptyList())
{_,old,new->
new.size>old.size
}
Ifanewlistwillnotcontainalargernumberofitemsthantheoldone,thenthevaluewillnotchange.Sowecantreatvetoablelikeobservable,whichisalsodecidingifthevalueshouldbechangedornot.Let'ssupposethatwewanttohavealistboundedtoview,butitneedstohavethreeelementsatleast.Wedon'tallowanychangethatwillmakeitpossibletohavefewerelements.Theimplementationwouldlookasfollows:
varlist:List<String>byDelegates.vetoable(emptyList())
{prop,old,new->
if(new.size<3)return@vetoablefalse//1
updateListView(new)
true//2
}
1. Ifanewlistsizeissmallerthan3,thenwedonotacceptit,andreturnfalsefromlambda.Thisfalsevaluereturnedbyreturnstatementwithlabel(thatisusedtothereturnfromthelambdaexpression)istheinformationthatthenewvalueshouldn'tbeaccepted.
2. Thislambdafunctionneedstoreturnavalue.Thisvalueistakeneitherfromreturnwithalabelorbythelastlineofthelambdabody.Herevaluetrueinformsthatanewvalueshouldbeaccepted.
Hereisasimpleexampleofitsusage:
listVetoable=listOf("A","B","C")//UpdateA,B,C
println(listVetoable)//Prints:[A,B,C]
listVetoable=listOf("A")//Nothinghappens
println(listVetoable)//Prints:[A,B,C]
listVetoable=listOf("A","B","C","D","E")
//Prints:[A,B,C,D,E]
Wecouldalsomakeitunchangeablebecauseofsomeotherreasons,forexample,wemightstillbeloadingthedata.Also,thevetoablepropertydelegatecanbeusedinvalidators.Forexample:
varname:StringbyDelegates.vetoable("")
{prop,old,new->
if(isValid(new)){
showNewData(new)
true
}else{
showNameError()
false
}
ThispropertycanbechangedonlytoavaluethatiscorrectaccordingtothepredicateisValid(new).
PropertydelegationtoMaptypeThestandardlibrarycontainsextensionsforMapandMutableMapwiththeStringkeytypethatprovidesthegetValueandsetValuefunctions.Thankstothem,mapcanalsobeusedasapropertydelegate:
classUser(map:Map<String,Any>){//1
valname:Stringbymap
valkotlinProgrammer:Booleanbymap
}
//Usage
valmap:Map<String,Any>=mapOf(//2
"name"to"Marcin",
"kotlinProgrammer"totrue
)
valuser=User(map)//3
println(user.name)//Prints:Marcin
println(user.kotlinProgrammer)//Prints:true
1. MapkeytypeneedstobeString,whilevaluetypeisnotrestricted.ItisoftenAnyorAny?
2. CreatingMapthatcontainsallthevalues3. Provideamaptoanobject.
ThiscanbeusefulwhenwearekeepingdatainMap,andalsoforfollowing:
WhenwewanttosimplifytheaccesstothesevaluesWhenwedefineastructurethatistellinguswhatkindofkeysweshouldexpectinthismapWhenweaskforapropertythatisdelegatedtoMap,itsvaluewillbetakenfromthismapvalueforakeyequaltothepropertyname
Howisitimplemented?Hereisthesimplifiedcodefromthestandardlibrary:
operatorfun<V,V1:V>Map<String,V>.getValue(//1
thisRef:Any?,//2
property:KProperty<*>):V1{//3
valkey=property.name//4
valvalue=get(key)
if(value==null&&!containsKey(key)){
throwNoSuchElementException("Key${property.name}
ismissinginthemap.")
}else{
returnvalueasV1//3
}
}
1. Visatypeofvalueonthelist2. thisRefisoftypeAny?,soMapcanbeusedaspropertydelegateinanycontext3. V1isreturntype.Thisisofteninferredfromproperty,butitmustbesubtype
oftypeV4. Nameofthepropertyisusedaskeyonmap.
Keepinmindthatthisisjustanextensionfunction.AllthatanobjectneedstobeadelegateistocontainthegetValuemethod(andsetValue,forread-writeproperties).Wecanevencreateadelegatefromanobjectofananonymousclassusingtheobjectdeclaration:
valsomePropertybyobject{//1
operatorfungetValue(thisRef:Any?,
property:KProperty<*>)="Something"
}
println(someProperty)//prints:Something
1. Objectisnotimplementinganyinterface.ItjustcontainsthegetValuemethodwithpropersignature.Anditisenoughtomakeitworkasaread-onlypropertydelegate.
Notethatinmapthereneedstobeanentrywithsuchanamewhenweareaskingforvalueofproperty,otherwiseanerrorwillbethrown(makingthepropertynullabledoesnotchangeit).
Delegatingfieldstomapcanbeuseful,forexample,whenwehaveanobjectfromanAPIwithdynamicfields.Wewouldliketotreattheprovideddataasanobjecttohaveeasieraccesstoitsfields,butwealsoneedtokeepitasamaptobeabletolistallthefieldsgivenbyanAPI(evenonesthatwewerenotexpecting).
Inthepreviousexample,weusedMap,whichisimmutable;therefore,theobjectpropertieswereread-only(val).Ifwewanttomakeanobjectthatcanbechanged,thenweshoulduseMutableMap,andthenthepropertiescanbedefinedasmutable(var).Hereisanexample:
classUser(valmap:MutableMap<String,Any>){
varname:Stringbymap
varkotlinProgrammer:Booleanbymap
overridefuntoString():String="Name:$name,
Kotlinprogrammer:$kotlinProgrammer"
}
//Usage
valmap=mutableMapOf(//1
"name"to"Marcin",
"kotlinProgrammer"totrue
)
valuser=User(map)
println(user)//prints:Name:Marcin,Kotlinprogrammer:true
user.map.put("name","Igor")//1
println(user)//prints:Name:Igor,Kotlinprogrammer:true
user.name="Michal"//2
println(user)//prints:Name:Michal,Kotlinprogrammer:true
1. Propertyvaluecanbechangedjustbychangingthevalueofthemap2. Propertyvaluecanbealsochangedlikeinanyotherproperty.Whatis
reallyhappeningthereisthatvaluechangeisdelegatedtosetValue,whichischangingmap.
Whilepropertiesherearemutable,thesetValuefunctionmustalsobeprovided.ItisimplementedasanextensionfunctionforMutableMap.Hereisthesimplifiedcode:
operatorfun<V>MutableMap<String,V>.setValue(
thisRef:Any?,
property:KProperty<*>,
value:V
){
put(property.name,value)
}
Notehowevensosimplefunctionscanallowsuchinnovativewayofusingthecommonobjects.Thisshowswhatpossibilitiespropertydelegatesaregiving.
Kotlinallowsustodefinecustomdelegates.Rightnow,wecanfindmanylibrariesthatprovidesnewpropertydelegatesthatcanbeusedfordifferentpurposesinAndroid.TherearevariouswaysinwhichpropertydelegationcanbeusedinAndroid.Inthenextsection,wewillseesomeexamplesofcustompropertydelegates,andwewilltakealookatcaseswherethisfeaturecanbereallyhelpful.
CustomdelegatesAllpreviousdelegatescamefromthestandardlibrary,butwecaneasilyimplementourownpropertydelegates.We'veseenthatinordertoallowaclasstobeadelegate,weneedtoprovidethegetValueandsetValuefunctions.Theymusthaveaconcretesignature,butthereisnoneedtoextendaclassorimplementtheinterface.Touseobjectasadelegate,wedon'tevenneedtochangeitsinternalimplementation,becausewecandefinegetValueandsetValueasextensionfunctions.However,whenwearecreatingcustomclassestobeadelegates,theninterfacemaybeuseful:
Itwoulddefinefunctionsstructure,sowecangeneratepropermethodsinAndroidStudio.Ifwearecreatinglibraries,thenwemightwanttomakedelegatesclassestobeprivateorinternaltopreventinappropriateusageofthem.We'veseenthissituationinthenotNullsection,wheretheclassNotNullVarwasprivateandservedasaReadWriteProperty<Any?,T>whichisaninterface.
InterfacesthatprovidefullfunctionalitytoallowsomeclasstobedelegateareReadOnlyProperty(forread-onlyproperties)andReadWriteProperty(forread-writeproperties).Theseinterfacesarereallyuseful,solet'slookattheirdefinitions:
publicinterfaceReadOnlyProperty<inR,outT>{
publicoperatorfungetValue(thisRef:R,
property:KProperty<*>):T
}
publicinterfaceReadWriteProperty<inR,T>{
publicoperatorfungetValue(thisRef:R,
property:KProperty<*>):T
publicoperatorfunsetValue(thisRef:R,
property:KProperty<*>,value:T)
}
Thevaluesofparameterswerealreadyexplained,butlet'slookatthemagain:
thisRef:Areferencetoanobjectwherethedelegateisused.Itstypedefinesthecontextinwhichthedelegatecanbeused.property:Areferencethatcontainsdataaboutadelegatedproperty.Itcontainsallinformationaboutthisproperty,suchasitsnameortype.
value:Anewvaluetoset.
TheparametersthisRefandpropertyarenotusedinthefollowingdelegates:Lazy,ObservableandVetoable.Map,MutableMap,andnotNullusepropertytoobtainthenameofthepropertyforthekey.Buttheseparameterscanbeusedindifferentcases.
Let'slookatsomesmall,butuseful,examplesofcustompropertydelegates.We'veseenthelazypropertydelegateforread-onlyproperties;however,sometimesweneedalazypropertythatismutable.Ifitwouldbeaskedforthevaluebeforeinitialization,thenitshouldfillitsvaluefromtheinitializerandreturnit.Inothercasesitshouldactlikeanormalmutableproperty:
fun<T>mutableLazy(initializer:()->T):ReadWriteProperty<Any?,T>=MutableLazy<T>(initializer)
privateclassMutableLazy<T>(valinitializer:()->T):ReadWriteProperty<Any?,T>{
privatevarvalue:T?=null
privatevarinitialized=false
overridefungetValue(thisRef:Any?,property:KProperty<*>):T{
synchronized(this){
if(!initialized){
value=initializer()
}
returnvalueasT
}
}
overridefunsetValue(thisRef:Any?,
property:KProperty<*>,value:T){
synchronized(this){
this.value=value
initialized=true
}
}
}
1. Thedelegateishiddenbehindtheinterfaceandservedbyafunction,andassuchallowsustochangetheimplementationofMutableLazywithoutworryingifitwillaffectthecodethatisusingit.
2. WeareimplementingReadWriteProperty.Itisoptional,butreallyusefulbecauseitisimposingthecorrectstructureofaread-writeproperty.ItsfirsttypeisAny?meaningthatweareallowedtousethispropertydelegateinanycontext,includingtop-level.Itssecondtypeisgeneric.Notethatthereisnorestrictionsonthistype,soitmightbenullabletoo.
3. Thevalueofthepropertyisstoredinthevalueproperty,anditsexistenceis
storedinaninitializedproperty.WeneedtodoitthiswaybecausewewanttoallowTtobeanullabletype.Thennullinthevaluecouldmeaneitherthatitwasnotyetinitializedorthatitisjustequaltonull.
4. Wedon'tneedtousetheoperatormodifier,becauseitisalreadyusedininterface.
5. IfgetValueiscalledbeforeanyvalueisset,thenthevalueisfilledusinginitializer.
6. WeneedtocastthevaluetoTbecauseitmightbenot-null,andweinitializedvalueasnullablewithnullasaninitialvalue.
Thispropertydelegatemightbeusefulindifferentuse-casesinAndroiddevelopment;forexample,whenadefaultvalueofapropertyisstoredinafile,andweneedtoreadit(whichisaheavyoperation):
vargameMode:GameModebyMutableLazy{
getDefaultGameMode()
}
varmapConfiguration:MapConfigurationbyMutableLazy{
getSavedMapConfiguration()
}
varscreenResolution:ScreenResolutionbyMutableLazy{
getOptimalScreenResolutionForDevice()
}
Thisway,ifausersetsacustomvalueofthispropertybeforeitsusage,wewon'thavetocalculateitourselves.Secondcustompropertydelegatewillallowustodefinepropertygetter:
vala:Intget()=1
valb:Stringget()="KOKO"
valc:Intget()=1+100
BeforeKotlin1.1definedit,wealwayshadtodefinethetypeofproperty.Toavoidit,wecandefinethefollowingextensionfunctiontofunctionaltype(therefore,alsothelambdaexpression):
inlineoperatorfun<R>(()->R).getValue(
thisRef:Any?,
property:KProperty<*>
):R=invoke()
Thenwecandefinethepropertieswithsimilarbehaviorthisway:
valaby{1}
valbby{"KOKO"}
valcby{1+100}
Thiswayisnotpreferredbecauseofitsdecreasedefficiency,butitisaniceexampleofpossibilitiesthatdelegatedpropertiesprovidesus.Suchasmallextensionfunctionismakingfunctionaltypetobepropertydelegate.Thisis,thesimplifiedcodeinKotlinaftercompilation(notethattheextensionfunctionismarkedasinline,soitscallswerereplacedwithitsbody):
privateval`a$delegate`={1}
vala:Intget()=`a$delegate`()
privateval`b$delegate`={"KOKO"}
valb:Stringget()=`b$delegate`()
privateval`c$delegate`={1+100}
valc:Intget()=`c$delegate`()
Inthenextsection,wearegoingtoseesomecustomdelegatescreatedforrealprojects.Theywillbepresentedtogetherwiththeproblemsthattheysolve.
ViewbingingWhenweareusingModel-View-Presenter(MVP)intheproject,thenweneedtomakeallthechangesinViewbyPresenter.Thus,weareforcedtocreatemultiplefunctionsontheview,suchas:
overridefungetName():String{
returnnameView.text.toString()
}
overridefunsetName(name:String){
nameView.text=name
}
Wealsohavetodefinethefunctionsinthefollowinginterface:
interfaceMainView{
fungetName():String
funsetName(name:String)
}
Wemaysimplifytheprecedingcodeandreducetheneedforsetter/gettermethodsbyusingpropertybinding.Wecanbindthepropertytoviewelement.Thisistheresultwewouldliketoachieve:
overridevarname:StringbybindToTex(R.id.textView)
Andinterface:
interfaceMainView{
varname:String
}
Theprecedingexampleismoreconciseandeasiertomaintain.NotethatweprovideelementIDbyargument.Asimpleclassthatwillgiveustheexpectedresultsisasfollows:
funActivity.bindToText(
@IdResviewId:Int)=object:
ReadWriteProperty<Any?,String>{
valtextViewbylazy{findViewById<TextView>(viewId)}
overridefungetValue(thisRef:Any?,
property:KProperty<*>):String{
returntextView.text.toString()
}
overridefunsetValue(thisRef:Any?,
property:KProperty<*>,value:String){
textView.text=value
}
}
Wecouldcreateasimilarbindingfordifferentviewpropertiesanddifferentcontexts(Fragment,Service).Anotherreallyusefultoolisbindingtovisibility,whichisbindingalogicalproperty(withthetypeBoolean)tothevisibilityofaviewelement:
funActivity.bindToVisibility(
@IdResviewId:Int)=object:
ReadWriteProperty<Any?,Boolean>{
valviewbylazy{findViewById(viewId)}
overridefungetValue(thisRef:Any?,
property:KProperty<*>):Boolean{
returnview.visibility==View.VISIBLE
}
overridefunsetValue(thisRef:Any?,
property:KProperty<*>,value:Boolean){
view.visibility=if(value)View.VISIBLEelseView.GONE
}
}
TheseimplementationsprovidepossibilitiesthatwouldbereallyhardtoachieveinJava.SimilarbindingsmightbecreatedforotherViewelementstomakeusingMVPshorterandsimpler.Thesnippetsthatwerejustpresentedareonlysimpleexamples,butbetterimplementationscanbefoundinthelibraryKotlinAndroidViewBindings(https://github.com/MarcinMoskala/KotlinAndroidViewBindings).
PreferencebindingToshowmorecomplexexamples,wewillpresenttheattempttohelpwiththeSharedPreferencesusage.TherearebetterKotlinapproachesforthisproblem,butthisattemptisnicetoanalyze,anditisareasonableexampleofweusepropertydelegateonextensionproperty.Asaresult,wewanttobeabletotreatvaluessavedinSharedPreferencesasiftheywerepropertiesofaSharedPreferencesobject.Hereisexampleusage:
preferences.canEatPie=true
if(preferences.canEatPie){
//Code
}
Wecanachieveitifwemakethefollowingextensionpropertydefinitions:
varSharedPreferences.canEatPie:
BooleanbybindToPreferenceField(true)//1
varSharedPreferences.allPieInTheWorld:
LongbybindToPreferenceField(0,"AllPieKey")//2
1. ThepropertyoftypeBoolean.Whenapropertyisnon-nullable,thandefaultvalueshavetobeprovidedinthefirstargumentoffunction.
2. Thepropertycanhavecustomkeyprovided.Itisusefulinreal-lifeprojects,wherewemusthavecontroloverthiskey(forexample,tonotchangeitunintentionallyduringpropertyrename).
Let'sanalyzehowitworksbydeepinvestigationofthenot-nullproperty.First,let'slookattheproviderfunctions.NotethatthetypeofthepropertyisdeterminingthewaythevalueistakenfromSharedPreferences(becausetherearedifferentfunctions,suchasgetString,getInt,andsoon).Toobtainit,weneedthisclasstypetobeprovidedasthereifiedtypeoftheinlinefunction,orthroughtheparameter.Thisiswhatadelegateproviderfunctionlookslike:
inlinefun<reifiedT:Any>bindToPreferenceField(
default:T?,
key:String?=null
):ReadWriteProperty<SharedPreferences,T>//1
=bindToPreferenceField(T::class,default,key)
fun<T:Any>bindToPreferenceField(//2
clazz:KClass<T>,
default:T?,
key:String?=null
):ReadWriteProperty<SharedPreferences,T>
=PreferenceFieldBinder(clazz,default,key)//1
1. BothfunctionsarereturningobjectbehindinterfaceReadWriteProperty<SharedPreferences,T>.NotethatcontexthereissettoSharedPreferences,soitcanbeusedonlythereorinSharedPreferencesextensions.Thisfunctionisdefinedbecausethetypeparametercannotberedefinedandweneedtoprovidetypeasanormalparameter.
2. NotethatthebindToPreferenceFieldfunctioncannotbeprivateorinternal,becauseinlinefunctionscanuseonlyfunctionswiththesameorlessrestrictedmodifiers.
Finally,let'sseethePreferenceFieldDelegateclass,whichisourdelegate:
internalopenclassPreferenceFieldDelegate<T:Any>(
privatevalclazz:KClass<T>,
privatevaldefault:T?,
privatevalkey:String?
):ReadWriteProperty<SharedPreferences,T>{
overrideoperatorfungetValue(thisRef:SharedPreferences,
property:KProperty<*>):T
=thisRef.getLong(getValue<T>(clazz,default,getKey(property))
overridefunsetValue(thisRef:SharedPreferences,
property:KProperty<*>,value:T){
thisRef.edit().apply
{putValue(clazz,value,getKey(property))}.apply()
}
privatefungetKey(property:KProperty<*>)=
key?:"${property.name}Key"
}
NowweknowhowthethisRefparameterisused.ItisofthetypeSharedPreferences,andwecanuseittogetandsetallthevalues.Herearedefinitionsofthefunctionsusedtogetandsavevaluesdependingonpropertytype:
internalfunSharedPreferences.Editor.putValue(clazz:KClass<*>,value:Any,key:String){
when(clazz.simpleName){
"Long"->putLong(key,valueasLong)
"Int"->putInt(key,valueasInt)
"String"->putString(key,valueasString?)
"Boolean"->putBoolean(key,valueasBoolean)
"Float"->putFloat(key,valueasFloat)
else->putString(key,value.toJson())
}
}
internalfun<T:Any>SharedPreferences.getValue(clazz:KClass<*>,default:T?,key:String):T=when(clazz.simpleName){
"Long"->getLong(key,defaultasLong)
"Int"->getInt(key,defaultasInt)
"String"->getString(key,defaultas?String)
"Boolean"->getBoolean(key,defaultasBoolean)
"Float"->getFloat(key,defaultasFloat)
else->getString(key,default?.toJson()).fromJson(clazz)
}asT
WealsoneedtoJsonandfromJsondefined:
varpreferencesGson:Gson=GsonBuilder().create()
internalfunAny.toJson()=preferencesGson.toJson(this)!!
internalfun<T:Any>String.fromJson(clazz:KClass<T>)=preferencesGson.fromJson(this,clazz.java)
WithsuchdefinitionswecandefineadditionalextensionpropertiestoSharedPreferences:
varSharedPreferences.canEatPie:BooleanbybindToPreferenceField(true)
Aswe'vealreadyseeninChapter7,ExtensionFunctionsandProperties,thereisnosuchthinginJavaasafieldthatwemightaddtoaclass.Underthehood,theextensionpropertyiscompiledtogetterandsetterfunctions,andtheyaredelegatingcallstoacreateddelegate:
val'canEatPie$delegate'=bindToPreferenceField(Boolean::class,true)
funSharedPreferences.getCanEatPie():Boolean{
return'canEatPie$delegate'.getValue(this,
SharedPreferences::canEatPie)
}
funSharedPreferences.setCanEatPie(value:Boolean){
'canEatPie$delegate'.setValue(this,SharedPreferences::canEatPie,
value)
}
Alsorememberthatextensionfunctionsare,infact,juststaticfunctionswithanextensiononthefirstparameter:
val'canEatPie$delegate'=bindToPreferenceField(Boolean::class,true)
fungetCanEatPie(receiver:SharedPreferences):Boolean{
return'canEatPie$delegate'.getValue(receiver,
SharedPreferences::canEatPie)
}
funsetCanEatPie(receiver:SharedPreferences,value:Boolean){
'canEatPie$delegate'.setValue(receiver,
SharedPreferences::canEatPie,value)
}
Presentedexamplesshouldbeenoughtounderstandhowpropertydelegatesareworkingandhowtheycanbeused.PropertydelegatesareusedintensivelyinKotlinopensourcelibraries.TheyareusedtomakefastandsimpleDependencyInjection(forexample,Kodein,Injekt,TornadoFX),bindingtoviews,SharedPreferencesorotherelements(attemptsalreadyshownincludesPreferenceHolder,andKotlinAndroidViewBindings),todefinepropertykeysonconfigurationdefinition(forexample,Konfig),oreventodefineadatabasecolumnstructure(forexample,Kwery).Stillthereisabigfieldofusagesthatarewaitingtobediscovered.
ProvidingadelegateSinceKotlin1.1,thereisanoperator,provideDelegate,thatisusedtoprovidedelegateduringclassinitialization.ThemainmotivationbehindprovideDelegatewasthatitallowstoprovidecustomizeddelegatedependingontraitsofproperty(name,type,annotations,andsoon).
TheprovideDelegateoperatorreturnsdelegate,andalltypesthathavethisoperatordonotneedtobedelegatesthemselvesinordertobeusedasadelegate.Hereisanexample:
classA(vali:Int){
operatorfunprovideDelegate(
thisRef:Any?,
prop:KProperty<*>
)=object:ReadOnlyProperty<Any?,Int>{
overridefungetValue(
thisRef:Any?,
property:KProperty<*>
)=i
}
}
valabyA(1)
Inthisexample,Aisusedasadelegate,whileitimplementsneithergetvaluenorsetvaluefunction.Thisispossible,becauseitdefinesaprovideDelegateoperator,whichreturnsthedelegatethatwillbeusedinsteadofA.Propertydelegationiscompiledintothefollowingcode:
privatevala$delegate=A().provideDelegate(this,this::prop)
vala:Int
get()=a1$delegate.getValue(this,this::prop)
PracticalexamplecanbefoundinKotlinsupportingpartoflibraryActivityStarter(https://github.com/MarcinMoskala/ActivityStarter).Activityargumentsaredefinedusingannotations,butwecanusepropertydelegationtosimplifyusagefromKotlinandallowpropertiesdefinitionaspossiblyread-onlyandnotlateinit:
@get:Arg(optional=true)valname:StringbyargExtra(defaultName)
@get:Arg(optional=true)valid:IntbyargExtra(defaultId)
@get:Argvalgrade:CharbyargExtra()
@get:Argvalpassing:BooleanbyargExtra()
Buttherearesomerequirements:
WhenargExtraisused,propertygetterhavetobeannotatedWeneedtospecifydefaultvalueifargumentisoptionalandtypeisnotnullable.
Tocheckthisrequirements,weneedreferencetopropertytogetgetterannotation.WecannothavesuchreferenceintheargExtrafunction,butwecanimplementtheminsideprovideDevegate:
fun<T>Activity.argExtra(default:T?=null)=ArgValueDelegateProvider(default)
fun<T>Fragment.argExtra(default:T?=null)=ArgValueDelegateProvider(default)
fun<T>android.support.v4.app.Fragment.argExtra(default:T?=null)=
ValueDelegateProvider(default)
classArgValueDelegateProvider<T>(valdefault:T?=null){
operatorfunprovideDelegate(
thisRef:Any?,
prop:KProperty<*>
):ReadWriteProperty<Any,T>{
valannotation=prop.getter.findAnnotation<Arg>()
when{
annotation==null->
throwError(ErrorMessages.noAnnotation)
annotation.optional&&!prop.returnType.isMarkedNullable&&
default==null->
throwError(ErrorMessages.optionalValueNeeded)
}
returnArgValueDelegate(default)
}
}
internalobjectErrorMessages{
constvalnoAnnotation=
"ElementgettermustbeannotatedwithArg"
constvaloptionalValueNeeded=
"Argumentsthatareoptionalandhavenot-
nullabletypemusthavedefautvaluespecified"
}
Suchdelegateisthrowingappropriateerrorwhenconditionisnotfulfilled:
vala:A?byArgValueDelegateProvider()
//Throwserrorduringinitialization:ElementgettermustbeannotatedwithArg
@get:Arg(optional=true)vala:AbyArgValueDelegateProvider()throwserrorduringinitialization:Argumentsthatareoptionalandhavenot-nullabletypemusthavedefaultvaluespecified.
Thiswayunacceptableargumentdefinitionsarethrowingappropriateerrorsduringobjectinitializationinsteadofbreakingapplicationinunexpected
situations.
SummaryInthischapter,wedescribedclassdelegate,propertydelegates,andhowtheycanbeusedtoremoveredundancyincode.Wedefinedadelegateasanobjecttowhichcallsfromotherobjectorpropertyaredelegatedto.WelearneddesignpatternsthatclassdelegationisstronglyconnectedtoDelegatepatternandDecoratorpattern.
Delegationpatternismentionedasanalternativetoinheritance,andDecoratorpatternisawaytoaddfunctionalitytodifferentkindsofclassesthatareimplementingthesameinterface.We'veseenhowpropertydelegationworks,andKotlinstandardlibrarypropertydelegates:notNull,lazy,observable,vetoable,andtheusageofMapasadelegate.Welearnedhowtheyworkandwhentheyshouldbeused.We'vealsoseenhowtomakeacustompropertydelegate,togetherwithexamplesofreal-lifeusage.
Knowledgeaboutdifferentfeaturesandtheirusageisnotenough--thereisalsoaneedtounderstandhowtheycanbeusedtogethertobuildgreatapplications.Inthenextchapter,wewillwriteademoapplicationandexplainhowthevariousKotlinfeaturesdescribedthroughoutthisbookcanbecombinedtogether.
MakingYourMarvelGalleryApplicationWe'vealreadyseenthemostimportantKotlinfeaturesthatallowustomakeAndroiddevelopmenteasierandmoreproductive,butitishardtounderstandthewholepicturejustbylookingatthepieces.Thisiswhy,inthischapter,wewillbuildawholeAndroidapplicationwritteninKotlin.
Itwasatoughdecisiontochoosewhatapplicationshouldbeimplementedinthischapter.Ithastobeshortandsimple,butatthesametimeitshouldutilizeasmanyKotlinfeaturesaspossible.Atthesametime,wewantedtominimizethenumberofusedlibraries,becauseitisabookaboutAndroiddevelopmentinKotlin,notaboutAndroidlibraries.Wewantedtomakeitlookasgoodaspossible,butwealsowantedtoavoidimplementationofcustomgraphicelements,becausetheyareusuallycomplexanddonotreallyprovidebenefitsfromaKotlinperspective.
WehavefinallydecidedtomakeaMarvelGalleryapplication--asmallappwhichwecanusetofindourfavoriteMarvelcharactersanddisplaytheirdetails.AlldataisprovidedfromtheMarvelwebsitebytheirAPI.
MarvelGalleryLet'simplementourMarvelGalleryapplication.Thisapplicationshouldallowthefollowingusecases:
Afterstartingtheapplication,theusercanseeagalleryofcharacters.Afterstartingtheapplication,theusercansearchforacharacterbyitsname.Whentheuserclicksonacharacterpicture,thereisaprofiledisplayed.Thecharacterprofilecontainscharactername,photo,description,anditsoccurrences.
Thesearethreeuse-casesthatdescribethemainfunctionalitiesoftheapplication.Inthenextsections,wearegoingtoimplementthemoneafteranother.Ifyouarelostduringthischapter,rememberthatyoucanalwaystakealookatthecompleteapplicationonGitHub(https://github.com/MarcinMoskala/MarvelGallery).
Tounderstandbetterwhatwewanttobuild,let'slookatsomescreenshotsfromthefinalversionofourapplication:
HowtousethischapterThischaptershowsallstepsandcodenecessarytobuildanapplication.Itspurposeistoshowthestep-by-stepprocessofthisapplicationdevelopment.Whenyouarereadingthischapter,concentrateonthedevelopmentprocessandtrytounderstandwhatthepurposeofpresentedcodeis.Youdon'tneedtofullyunderstandlayoutsandyoudon'thavetounderstandunittestdefinitionsaslongasyouunderstandwhattheyaredoing.ConcentrateonapplicationstructureandKotlinsolutionsthataremakingthefinalcodesimpler.Mostsolutionswerealreadydescribedinpreviouschapters,sotheyhaveonlyabriefdescription.Thevalueinthischapteristhattheirusageispresentedinthecontextofaconcreteapplication.
YoucandownloadtheapplicationcodefromGitHub(https://github.com/MarcinMoskala/MarvelGallery).
OnGitHub,youcanseethefinalcode,downloadit,oryoucancloneittoyourcomputerusingGit:
gitclonegit@github.com:MarcinMoskala/MarvelGallery.git
TheapplicationalsoincludesUItestswritteninEspresso,buttheyarenotpresentedonthischaptertomakeitsimplerforreaderswhoarenotproficientinEspressousage.
EachsectionofthischapterhasacorrespondingGitbranchonthisproject,soifyouwanttoseehowthecodelooksattheendofthesectionthenyoucanjustswitchtothecorrespondingbranch:
Also,locally,afteryouclonetherepository,youcancheckoutthecorrespondingbranchusingthefollowingGitcommand:
gitcheckoutCharacter_search
Ifyouhaveanelectronicversionofthisbookandyouwanttomakethewholeapplicationbycopyandpastingpartsofthecodethenyoucandoit,butremembertoplacefilesinthefolderscorrespondingtothepackage.Thisway,youwillkeepacleanstructureintheproject.
Notethatifyouplacecodefromthebookinanotherfolder,therewillbeawarningdisplayed:
Youcanintentionallyplaceafileinanyfolder,becausethesecondfixpropositionistomovethefileintothepathcorrespondingtothedefinedpackage:
Youcanuseittomoveyourfileintothecorrectlocation.
MakeanemptyprojectBeforewecanstartimplementingfunctionalities,weneedtocreateanemptyKotlinAndroidprojectwithasingleactivity,MainActivty.ThisprocesswasdescribedinChapter1,BeginningyourKotlinAdventure.Therefore,wedon'tneedtodescribeitdeeply,butwewillshowwhatthestepsareinAndroidStudio3.0:
1. Setname,package,andlocationforthenewproject.RemembertotickIncludeKotlinsupportoption:.
2. WecanchooseotherminimalAndroidversion,butinthisexample,wearegoingtosetAPI16:
3. Chooseatemplate.Wedon'tneedanyofthesetemplatessoweshouldstartfromEmptyActivity:
4. Namenewlycreatedactivity.WecankeepthefirstviewnamedMainActivity:
ForAndroidStudiopriorto3.x,weneedtofollowslightlydifferentsteps:
CreateaprojectfromtemplatewithemptyActivity.
1.ConfigureKotlinintheproject(forexample,Ctrl/Cmd+Shift+AandConfigureKotlininproject).2.ConvertallJavaclassestoKotlin(forexample,inMainActivityCtrl/Cmd+Shift+AandConvertJavafiletoKotlinfile).
Afterthesesteps,wewillhaveaKotlinAndroidapplicationwithanemptyActivitycreated:
CharactergalleryInthissection,wewillimplementasingleusecase--afterstartingtheapplication,theusercanseeagalleryofcharacters.
Thisisaprettycomplexusecasebecauseitrequiresviewtobepresented,networkconnectionwithAPIandbusinessrulesimplementation.Therefore,wewillsplititintothefollowingtasks:
ViewimplementationCommunicationwithAPIBusinesslogicimplementationofcharacterdisplayPuttingitalltogether
Suchtasksaremucheasiertoimplement.Let'simplementthemoneafteranother.
ViewimplementationLet'sstartwithViewimplementation.Here,wearegoingtodefinewhatthelistofcharacterswilllooklike.Fortestingpurposes,wearealsogoingtodefineafewcharactersanddisplaythem.
Let'sstartwithMainActivitylayoutimplementation.WewilluseRecyclerViewtoshowalistofelements.TheRecyclerViewlayoutisdistributedinaseparatedependency,whichwehavetoaddtotheappmodulebuild.gradlefile:
implementation"com.android.support:recyclerview-v7:$android_support_version"
Theandroid_support_versioninstanceisavariablewhichisnotyetdefined.ThereasonbehinditisthattheversionshouldbethesameforallAndroidsupportlibrariesandwhenweextractthisversionnumberasaseparatorvariablethenitiseasiertomanage.ThisiswhyweshouldreplacethehardcodedversionforeachoftheAndroidsupportlibrarieswithareferencetoandroid_support_version:
implementation"com.android.support:appcompat-
v7:$android_support_version"
implementation"com.android.support:design:$android_support_version"
implementation"com.android.support:support-
v4:$android_support_version"
implementation"com.android.support:recyclerview-
v7:$android_support_version"
Andwehavetosetsupportlibraryversionvalue.Goodpracticeistodefineitintheprojectbuild.gradlefileinsidebuildscript,afterthekotlin_versiondefinition:
ext.kotlin_version='1.1.4-2'
ext.android_support_version="26.0.1"
NowwecanstartimplementationofMainActivitylayout.Thisistheeffectthatwewanttoachieve:
WewillkeepcharacterelementsonRecyclerViewpackedintoSwipeRefreshLayouttoallowswipe-refresh.Also,tofulfillMarvelcopyright,thereneedstobeapresentedlabelthatisinformingthatdataisprovidedbyMarvel.Thelayoutactivity_main(res/layout/activity_main.xml)shouldbereplacedwithfollowingdefinition:
<?xmlversion="1.0"encoding="utf-8"?>
<RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/charactersView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:fitsSystemWindows="true">
<android.support.v4.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/swipeRefreshView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"/>
</android.support.v4.widget.SwipeRefreshLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="@android:color/white"
android:gravity="center"
android:text="@string/marvel_copyright_notice"/>
</RelativeLayout>
Weneedtoaddacopyrightnoticetostrings(res/values/strings.xml):
<stringname="marvel_copyright_notice">
DataprovidedbyMarvel.©2017MARVEL
</string>
Hereisapreview:
Thenextstepistodefinetheitemview.Wewouldlikeeachelementtobealwayssquare.Todothis,weneedtodefineaviewwhichwillpreservethesquareshape(placeitinview/views):
packagecom.sample.marvelgallery.view.views
importandroid.util.AttributeSet
importandroid.widget.FrameLayout
importandroid.content.Context
classSquareFrameLayout@JvmOverloadsconstructor(//1
context:Context,
attrs:AttributeSet?=null,
defStyleAttr:Int=0
):FrameLayout(context,attrs,defStyleAttr){
overridefunonMeasure(widthMeasureSpec:Int,
heightMeasureSpec:Int){
super.onMeasure(widthMeasureSpec,widthMeasureSpec)//2
}
}
1. UsingJvmOverloadsannotation,we'veavoidedtelescopingconstructorsthatarenormallyusedtodefineacustomviewinAndroid.ThiswasdescribedinChapter4,ClassesandObjects.
2. Weareforcingtheelementtohavealwaysthesameheightaswidth.
WithSquareFrameLayout,wecandefinethelayoutofgalleryitems.Thisiswhatwewantittolooklike:
WeneedtodefineImageViewtodisplaythecharacterimage,andTextViewtodisplayitsname.WhileSquareFrameLayoutisactuallyFrameLayoutthathasfixedheight,itschildrenelements(imageandtext)arebydefaultplacedoneaboveanother.Let'saddlayoutintoitem_character.xmlfileinres/layout:
//./res/layout/item_character.xml
<com.sample.marvelgallery.view.views.SquareFrameLayoutxmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="horizontal"
android:padding="@dimen/element_padding">
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:shadowColor="#111"
android:shadowDx="5"
android:shadowDy="5"
android:shadowRadius="0.01"
android:textColor="@android:color/white"
android:textSize="@dimen/standard_text_size"
tools:text="Somename"/>
</com.sample.marvelgallery.view.views.SquareFrameLayout>
Notethatwearealsousingvaluessuchaselement_paddingdefinedindimens.Let'saddthemtothedimen.xmlfileinres/values:
<?xmlversion="1.0"encoding="utf-8"?>
<resources>
<dimenname="character_header_height">240dp</dimen>
<dimenname="standard_text_size">20sp</dimen>
<dimenname="character_description_padding">10dp</dimen>
<dimenname="element_padding">10dp</dimen>
</resources>
Aswecansee,eachelementneedstodisplaythenameofthecharacteranditsimage.Therefore,themodelofacharacterneedstocontainthesetwoproperties.Let'sdefineasimplemodelforacharacter:
packagecom.sample.marvelgallery.model
dataclassMarvelCharacter(
valname:String,
valimageUrl:String
)
TodisplayalistofelementsusingRecyclerView,weneedtoimplementbothaRecyclerViewlistandanitemadapter.Alistadapterisusedtomanageallelementsinalist,whileanitemadapterisanadapterforasingleitemtype.Here,weneedonlyoneitemadapter,becausewedisplayasingletypeofitems.Itis,however,goodpracticetoassumethatinfuturetheremightbeotherkindofelementsonthislist,forexample,comicsorads.Thesamewiththelistadapter--weneed
onlyoneinthisexample,butinmostprojectsthereismorethanasinglelistanditisbettertoextractcommonbehaviorintoasingleabstractclass.
WhilethisexampleisdesignedtopresenthowKotlincanbeusedinlargerprojects,wewilldefineanabstractlistadapter,whichwewillnameRecyclerListAdapter,andanabstractitemadapter,whichwewillnameItemAdapter.HereistheItemAdapterdefinition:
packagecom.sample.marvelgallery.view.common
importandroid.support.v7.widget.RecyclerView
importandroid.support.annotation.LayoutRes
importandroid.view.View
abstractclassItemAdapter<T:RecyclerView.ViewHolder>
(@LayoutResopenvallayoutId:Int){//1
abstractfunonCreateViewHolder(itemView:View):T//2
@Suppress("UNCHECKED_CAST")//1
funbindViewHolder(holder:RecyclerView.ViewHolder){
(holderasT).onBindViewHolder()//1
}
abstractfunT.onBindViewHolder()//1,3
}
1. Weneedtopassaholderasatypeparametertoallowdirectoperationsonitsfields.TheholderiscreatedinonCreateViewHoldersoweknowthatitstypewillbealwaystypeparameterT.Therefore,wecancasttheholdertoTonbindViewHolderanduseitasareceiverobjectforonBindViewHolder.Suppression@Suppress("UNCHECKED_CAST")isherejusttohidethewarningwhileweknowthatwecansecurelycastinthissituation.
2. Functionusedtocreateviewholder.Inmostcases,itwillbeasingleexpressionfunctionthatisjustcallingaconstructor.
3. IntheonBin+dViewHolderfunction,wewillsetallvaluesonitemview.
HereisthedefinitionofRecyclerListAdapter:
packagecom.sample.marvelgallery.view.common
importandroid.support.v7.widget.RecyclerView
importandroid.view.LayoutInflater
importandroid.view.ViewGroup
openclassRecyclerListAdapter(//1
varitemsList<AnyItemAdapter>=listOf()
):RecyclerView.Adapter<RecyclerView.ViewHolder>(){
overridefinalfungetItemCount()=items.size//4
overridefinalfungetItemViewType(position:Int)=
items[position].layoutId//3,4
overridefinalfunonCreateViewHolder(parent:ViewGroup,
layoutId:Int):RecyclerView.ViewHolder{//4
valitemView=LayoutInflater.from(parent.context)
.inflate(layoutId,parent,false)
returnitems.first
{it.layoutId==layoutId}.onCreateViewHolder(itemView)//3
}
overridefinalfunonBindViewHolder
(holder:RecyclerView.ViewHolder,position:Int){//4
items[position].bindViewHolder(holder)
}
}
typealiasAnyItemAdapter=ItemAdapter
<outRecyclerView.ViewHolder>//5
1. Classisopeninsteadofabstractbecauseitcanbeinitializedandusedwithoutanychildren.Wedefinechildrentoallowustodefinecustommethodsfordifferentlists.
2. Wekeepitemsinlist.3. Wewilluselayouttodistinguishitemtype.Becauseofit,wecannotuse
twoitemadapterswiththesamelayoutonthesamelist,butthissolutionissimplifyingalot.
4. MethodsareoverridingmethodsofRecyclerView.Adapter,buttheyalsousefinalmodifiertorestricttheiroverrideinchildren.AlllistadaptersthatareextendingRecyclerListAdaptershouldoperateonitems.
5. WedefinetypealiastosimplifythedefinitionofanyItemAdapter.
Usingtheprecedingdefinitions,wecandefineMainListAdapter(adapterforcharacterlist)andCharacterItemAdapter(adapterforitemonlist).HereisthedefinitionofMainListAdapter:
packagecom.sample.marvelgallery.view.main
importcom.sample.marvelgallery.view.common.AnyItemAdapter
importcom.sample.marvelgallery.view.common.RecyclerListAdapter
classMainListAdapter(items:List<AnyItemAdapter>):RecyclerListAdapter(items)
Inthisproject,wedonotneedanyspecialmethodsdefinedinMainListAdapter,buttoshowhoweasyitistodefinethem,hereispresentedMainListAdapterwithadditionalmethodstoaddanddelete:
classMainListAdapter(items:List<AnyItemAdapter>):RecyclerListAdapter(items){
funadd(itemAdapter:AnyItemAdapter){
items+=itemAdapter)
valindex=items.indexOf(itemAdapter)
if(index==-1)return
notifyItemInserted(index)
}
fundelete(itemAdapter:AnyItemAdapter){
valindex=items.indexOf(itemAdapter)
if(index==-1)return
items-=itemAdapter
notifyItemRemoved(index)
}
}
HereisthedefinitionofCharacterItemAdapter:
packagecom.sample.marvelgallery.view.main
importandroid.support.v7.widget.RecyclerView
importandroid.view.View
importandroid.widget.ImageView
importandroid.widget.TextView
importcom.sample.marvelgallery.R
importcom.sample.marvelgallery.model.MarvelCharacter
importcom.sample.marvelgallery.view.common.ItemAdapter
importcom.sample.marvelgallery.view.common.bindView
importcom.sample.marvelgallery.view.common.loadImage
classCharacterItemAdapter(
valcharacter:MarvelCharacter//1
):ItemAdapter<CharacterItemAdapter.ViewHolder>(R.layout.item_character){
overridefunonCreateViewHolder(itemView:View)=ViewHolder(itemView)
overridefunViewHolder.onBindViewHolder(){//2
textView.text=character.name
imageView.loadImage(character.imageUrl)//3
}
classViewHolder(itemView:View):RecyclerView.ViewHolder(itemView)
{
valtextViewbybindView<TextView>(R.id.textView)//4
valimageViewbybindView<ImageView>(R.id.imageView)//4
}
}
1. MarvelCharacterispassedbyconstructor.2. onBindViewHoldermethodisusedsetupviews.Itwasdefinedasanabstract
memberextensionfunctioninItemAdapterand,thankstothat,nowwecanusetextViewandimageViewexplicitlyinsideitsbody.
3. FunctionloadImageisnotdefinedyet.Wewilldefineitasanextensionfunctionabitlater.
4. Inviewholder,wearebindingpropertiestoviewelementsusingthe
bindViewfunctionthatwillsoonbedefined.
Inside,weusethefunctionsloadImageandbindViewwhicharenotyetdefined.bindViewisatop-levelextensionfunctiontoRecyclerView.ViewHolder,whichisprovidingalazydelegatethatisprovidingaviewfoundbyitsID:
//ViewExt.kt
packagecom.sample.marvelgallery.view.common
importandroid.support.v7.widget.RecyclerView
importandroid.view.View
fun<T:View>RecyclerView.ViewHolder.bindView(viewId:Int)
=lazy{itemView.findViewById<T>(viewId)}
WealsoneedtodefinetheloadImageextensionfunctionthatwillhelpustodownloadanimagefromtheURLandplaceitintoImageView.TwotypicallibrariesusedtosuchpurposearePicassoandGlide.WewilluseGlide,andtodoit,weneedtoaddadependencyinbuild.gradle:
implementation"com.android.support:recyclerview-
v7:$android_support_version"
implementation"com.github.bumptech.glide:glide:$glide_version"
Specifytheversioninprojectbuild.gradle:
ext.android_support_version="26.0.0"
ext.glide_version="3.8.0"
AddpermissiontousetheinternetinAndroidManifest:
<manifestxmlns:android="http://schemas.android.com/apk/res/android"
package="com.sample.marvelgallery">
<uses-permissionandroid:name="android.permission.INTERNET"/>
<application
...
AndwecanfinallydefinetheloadImageextensionfunctionfortheImaveViewclass:
//ViewExt.kt
packagecom.sample.marvelgallery.view.common
importandroid.support.v7.widget.RecyclerView
importandroid.view.View
importandroid.widget.ImageView
importcom.bumptech.glide.Glide
fun<T:View>RecyclerView.ViewHolder.bindView(viewId:Int)
=lazy{itemView.findViewById<T>(viewId)}
funImageView.loadImage(photoUrl:String){
Glide.with(context)
.load(photoUrl)
.into(this)
}
Itistimetodefinetheactivitythatwilldisplaythislist.Wewilluseonemoreelement,theKotlinAndroidextensionsplugin.Itisusedtosimplifyaccesstoviewelementsfromcode.Itsusageissimple--weaddthekotlin-android-extensionsplugininmodulebuild.gradle:
applyplugin:'com.android.application'
applyplugin:'kotlin-android'
applyplugin:'kotlin-android-extensions'
Andwehavesomeviewdefinedinlayout:
<TextView
android:id="@+id/nameView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
ThenwecanimportareferencetothisviewinsideActivity:
importkotlinx.android.synthetic.main.activity_main.*
AndwecanaccessViewelementsdirectlyusingitsnamewithoutusingthefindViewByIdmethodordefineannotations:
nameView.text="Somename"
WewilluseKotlinAndroidextensionsinallactivitiesintheproject.Nowlet'sdefineMainActivitytodisplayalistofcharacterswithimages:
packagecom.sample.marvelgallery.view.main
importandroid.os.Bundle
importandroid.support.v7.app.AppCompatActivity
importandroid.support.v7.widget.GridLayoutManager
importandroid.view.Window
importcom.sample.marvelgallery.R
importcom.sample.marvelgallery.model.MarvelCharacter
importkotlinx.android.synthetic.main.activity_main.*
classMainActivity:AppCompatActivity(){
privatevalcharacters=listOf(//1
MarvelCharacter(name="3-DMan",imageUrl="http://i.annihil.us/u/prod/marvel/i/mg/c/e0/535fecbbb9784.jpg"),
MarvelCharacter(name="Abomination(EmilBlonsky)",imageUrl="http://i.annihil.us/u/prod/marvel/i/mg/9/50/4ce18691cbf04.jpg")
)
overridefunonCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)//2
setContentView(R.layout.activity_main)
recyclerView.layoutManager=GridLayoutManager(this,2)//3
valcategoryItemAdapters=characters
.map(::CharacterItemAdapter)//4
recyclerView.adapter=MainListAdapter(categoryItemAdapters)
}
}
1. Herewedefineatemporarylistofcharacterstodisplay.2. Weusethiswindowfeaturebecausewedon'twanttodisplayatitle.3. WeuseGridLayoutManagerasRecyclerViewlayoutmanagertoachieveagrid
effect.4. WearecreatingitemadaptersfromcharactersusingtheCharacterItemAdapter
constructorreference.
Nowwecancompiletheprojectandwewillseethefollowingscreen:
NetworkdefinitionUntilnow,thepresenteddatawashardcodedinsidetheapplication,butwewanttousedatafromtheMarvelAPIinstead.Todoit,weneedtodefinesomenetworkmechanismsthatwillretrievethedatafromtheserver.WearegoingtouseRetrofit,apopularAndroidlibraryusedtosimplifynetworkoperations,togetherwithRxJava,apopularlibraryusedforreactiveprogramming.Forbothlibraries,wewilluseonlybasicfunctionalitiestomaketheirusageassimpleaspossible.Tousethem,weneedtoaddfollowingdependenciesinthemodulebuild.gradle:
dependencies{
implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:
$kotlin_version"
implementation"com.android.support:appcompat-v7:
$android_support_version"
implementation"com.android.support:recyclerview-v7:
$android_support_version"
implementation"com.github.bumptech.glide:glide:$glide_version"
//RxJava
implementation"io.reactivex.rxjava2:rxjava:$rxjava_version"
//RxAndroid
implementation"io.reactivex.rxjava2:rxandroid:$rxandroid_version"
//Retrofit
implementation(["com.squareup.retrofit2:retrofit:$retrofit_version",
"com.squareup.retrofit2:adapter-
rxjava2:$retrofit_version",
"com.squareup.retrofit2:converter-
gson:$retrofit_version",
"com.squareup.okhttp3:okhttp:$okhttp_version",
"com.squareup.okhttp3:logging-
interceptor:$okhttp_version"])
testImplementation'junit:junit:4.12'
androidTestImplementation
'com.android.support.test:runner:1.0.0'
androidTestImplementation
'com.android.support.test.espresso:espresso-core:3.0.0'
}
Andversiondefinitionsinprojectbuild.gradle:
ext.kotlin_version='1.1.3-2'
ext.android_support_version="26.0.0"
ext.glide_version="3.8.0"
ext.retrofit_version='2.2.0'
ext.okhttp_version='3.6.0'
ext.rxjava_version="2.1.2"
ext.rxandroid_version='2.0.1'
WealreadyhaveinternetpermissiondefinedonAndroidManifest,sowedon'tneedtoaddit.AsimpleRetrofitdefinitionmightlooklikethefollowing:
valretrofitbylazy{makeRetrofit()}//1
privatefunmakeRetrofit():Retrofit=Retrofit.Builder()
.baseUrl("http://gateway.marvel.com/v1/public/")//2
.build()
1. Wecankeepretrofitinstanceaslazytop-levelproperty.2. HerewedefinethebaseUrl
ButtherearesomeadditionalrequirementsonRetrofitthatneedtobematched.WeneedtoaddconverterstouseRetrofittogetherwithRxJava,andtosendobjectsserializedasJSON.WealsoneedinterceptorsthatwillbeusedtoprovideheadersandextraqueriesneededbyMarvelAPI.Thisisasmallapplication,sowecandefineallrequiredelementsastop-levelfunctions.ThefullRetrofitdefinitionwillbethefollowing:
//Retrofit.kt
packagecom.sample.marvelgallery.data.network.provider
importcom.google.gson.Gson
importokhttp3.OkHttpClient
importretrofit2.Retrofit
importretrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
importretrofit2.converter.gson.GsonConverterFactory
importjava.util.concurrent.TimeUnit
valretrofitbylazy{makeRetrofit()}
privatefunmakeRetrofit():Retrofit=Retrofit.Builder()
.baseUrl("http://gateway.marvel.com/v1/public/")
.client(makeHttpClient())
.addConverterFactory(GsonConverterFactory.create(Gson()))//1
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())//2
.build()
privatefunmakeHttpClient()=OkHttpClient.Builder()
.connectTimeout(60,TimeUnit.SECONDS)//3
.readTimeout(60,TimeUnit.SECONDS)//4
.addInterceptor(makeHeadersInterceptor())//5
.addInterceptor(makeAddSecurityQueryInterceptor())//6
.addInterceptor(makeLoggingInterceptor())//7
.build()
1. AddaconverterthatallowsobjectJSONserializationanddeserializationusingGSONlibrary.
2. AddaconverterthatwillallowRxJava2types(Observable,Single)as
observablesforreturnedvaluesfromnetworkrequests.3. Weaddcustominterceptors.Weneedtodefineallofthem.
Let'sdefinetheneededinterceptors.makeHeadersInterceptorisusedtoaddstandardheadersforeachrequest:
//HeadersInterceptor.kt
packagecom.sample.marvelgallery.data.network.provider
importokhttp3.Interceptor
funmakeHeadersInterceptor()=Interceptor{chain->//1
chain.proceed(chain.request().newBuilder()
.addHeader("Accept","application/json")
.addHeader("Accept-Language","en")
.addHeader("Content-Type","application/json")
.build())
}
1. InterceptorisSAM,sowecandefineitusingaSAMconstructor.
ThemakeLoggingInterceptorfunctionisusedtodisplaylogsonconsolewhenwearerunningtheapplicationindebugmode:
//LoggingInterceptor.kt
packagecom.sample.marvelgallery.data.network.provider
importcom.sample.marvelgallery.BuildConfig
importokhttp3.logging.HttpLoggingInterceptor
funmakeLoggingInterceptor()=HttpLoggingInterceptor().apply{
level=if(BuildConfig.DEBUG)HttpLoggingInterceptor.Level.BODY
elseHttpLoggingInterceptor.Level.NONE
}
ThemakeAddRequiredQueryInterceptorfunctionismorecomplex,becauseitisusedtoprovidequeryparametersusedbyMarvelAPItoverifytheuser.TheseparametersneedahashcalculatedusingtheMD5algorithm.ItalsoneedsapublicandprivatekeyfromtheMarvelAPI.Everyonecangeneratetheirownkeysathttps://developer.marvel.com/.Onceyouhavegeneratedkeys,weneedtoplacetheminthegradle.propertiesfile:
org.gradle.jvmargs=-Xmx1536m
marvelPublicKey=REPLEACE_WITH_YOUR_PUBLIC_MARVEL_KEY
marvelPrivateKey=REPLEACE_WITH_YOUR_PRIVATE_MARVEL_KEY
Alsoaddthefollowingdefinitionsinthemodulebuild.gradleinAndroidinthedefaultConfigsection:
defaultConfig{
applicationId"com.sample.marvelgallery"
minSdkVersion16
targetSdkVersion26
versionCode1
versionName"1.0"
testInstrumentationRunner
"android.support.test.runner.AndroidJUnitRunner"
buildConfigField("String","PUBLIC_KEY","\"${marvelPublicKey}\"")
buildConfigField("String","PRIVATE_KEY","\"${marvelPrivateKey}\"")
}
Afterprojectrebuild,youwillbeabletoaccessthesevaluesbyBuildConfig.PUBLIC_KEYandBuildConfig.PRIVATE_KEY.Usingthesekeys,wecangeneratequeryparametersthatarerequiredbyMarvelAPI:
//QueryInterceptor.kt
packagecom.sample.marvelgallery.data.network.provider
importcom.sample.marvelgallery.BuildConfig
importokhttp3.Interceptor
funmakeAddSecurityQueryInterceptor()=Interceptor{chain->
valoriginalRequest=chain.request()
valtimeStamp=System.currentTimeMillis()
//Urlcustomization:addqueryparameters
valurl=originalRequest.url().newBuilder()
.addQueryParameter("apikey",BuildConfig.PUBLIC_KEY)//1
.addQueryParameter("ts","$timeStamp")//1
.addQueryParameter("hash",calculatedMd5(timeStamp.toString()+BuildConfig.PRIVATE_KEY+BuildConfig.PUBLIC_KEY))//1
.build()
//Requestcustomization:setcustomurl
valrequest=originalRequest
.newBuilder()
.url(url)
.build()
chain.proceed(request)
}
1. Weneedtoprovidethreeadditionalqueries:apikey:Whichisjustincludingourpublickey.ts:Whichisjustcontainingdevicetimeinmilliseconds.Itisusedtoimprovethesecurityofthehashprovidedinthenextquery.hash:WhichiscalculatedasMD5hashfromtimestamp,private,andpublickey,oneafteranotherinasingleString.
HereisthedefinitionofthefunctionusedtocalculatetheMD5hash:
//MD5.kt
packagecom.sample.marvelgallery.data.network.provider
importjava.math.BigInteger
importjava.security.MessageDigest
/**
*CalculateMD5hashfortext
*@paramtimeStampCurrenttimeStamp
*@returnMD5hashstring
*/
funcalculatedMd5(text:String):String{
valmessageDigest=getMd5Digest(text)
valmd5=BigInteger(1,messageDigest).toString(16)
return"0"*(32-md5.length)+md5//1
}
privatefungetMd5Digest(str:String):ByteArray=MessageDigest.getInstance("MD5").digest(str.toByteArray())
privateoperatorfunString.times(i:Int)=(1..i).fold(""){acc,_->acc+this}
1. Weareusingthetimesextensionoperatortofillthehashwithzerosifitisshorterthan32.
Wehaveinterceptorsdefined,sowecandefineactualAPImethods.TheMarvelAPIcontainsalotofdatamodelsthatarerepresentingcharacters,lists,andsoon.Weneedtodefinethemasseparateclasses.Suchclassesarecalleddatatransferobjects(DTOs).Wewilldefineobjectswewillneed:
packagecom.sample.marvelgallery.data.network.dto
classDataContainer<T>{
varresults:T?=null
}
packagecom.sample.marvelgallery.data.network.dto
classDataWrapper<T>{
vardata:DataContainer<T>?=null
}
packagecom.sample.marvelgallery.data.network.dto
classImageDto{
lateinitvarpath:String//1
lateinitvarextension:String//1
valcompleteImagePath:String
get()="$path.$extension"
}
packagecom.sample.marvelgallery.data.network.dto
classCharacterMarvelDto{
lateinitvarname:String//1
lateinitvarthumbnail:ImageDto//1
valimageUrl:String
get()=thumbnail.completeImagePath
}
1. Forvaluesthatmightnotbeprovided,weshouldsetadefaultvalue.Valuesthataremandatorymightbeprefixedwithlateinitinstead.
RetrofitisusingreflectiontocreateanHTTPrequestbasingofinterfacedefinition.ThisishowwecanimplementaninterfacethatisdefininganHTTPrequest:
packagecom.sample.marvelgallery.data.network
importcom.sample.marvelgallery.data.network.dto.CharacterMarvelDto
importcom.sample.marvelgallery.data.network.dto.DataWrapper
importio.reactivex.Single
importretrofit2.http.GET
importretrofit2.http.Query
interfaceMarvelApi{
@GET("characters")
fungetCharacters(
@Query("offset")offset:Int?,
@Query("limit")limit:Int?
):Single<DataWrapper<List<CharacterMarvelDto>>>
}
Withsuchdefinitions,wecanfinallygetalistofcharacters:
retrofit.create(MarvelApi::class.java)//1
.getCharacters(0,100)//2
.subscribe({/*code*/})//3
1. WeusearetrofitinstancetocreateanobjectthatwillmakeHTTPrequestsaccordingtotheMarvelApiinterfacedefinition.
2. WecreateobservablereadytosendcalltoAPI.3. Bysubscribe,wesendanHTTPrequestandwestartlisteningforaresponse.
Thefirstargumentisthecallbackthatisinvokedwhenwesuccessfullyreceivearesponse.
Suchanetworkdefinitioncouldbesufficient,butwemightimplementitbetter.ThebiggestproblemisthatwenowneedtooperateonDTOobjectsinsteadofonourowndatamodelobjects.Formapping,weshoulddefineanadditionallayer.Therepositorypatternisusedforthispurpose.Thispatternisalsoreallyhelpfulwhenweareimplementingunittests,becausewecanmocktherepositoryinsteadofthewholeAPIdefinition.Thisisthedefinitionofrepositorythatwewouldliketohave:
packagecom.sample.marvelgallery.data
importcom.sample.marvelgallery.model.MarvelCharacter
importio.reactivex.Single
interfaceMarvelRepository{
fungetAllCharacters():Single<List<MarvelCharacter>>
}
AndhereistheimplementationofMarvelRepository:
packagecom.sample.marvelgallery.data
importcom.sample.marvelgallery.data.network.MarvelApi
importcom.sample.marvelgallery.data.network.provider.retrofit
importcom.sample.marvelgallery.model.MarvelCharacter
importio.reactivex.Single
classMarvelRepositoryImpl:MarvelRepository{
valapi=retrofit.create(MarvelApi::class.java)
overridefungetAllCharacters():Single<List<MarvelCharacter>>=api.getCharacters(
offset=0,
limit=elementsOnListLimit
).map{
it.data?.results.orEmpty().map(::MarvelCharacter)//1
}
companionobject{
constvalelementsOnListLimit=50
}
}
1. WearegettingalistofDTOelementsandmappingitintoMarvelCharacterusingaconstructorreference.
Tomakeitwork,weneedtodefineanadditionalconstructorinMarvelCharacter,thattakesCharacterMarvelDtoasanargument:
packagecom.sample.marvelgallery.model
importcom.sample.marvelgallery.data.network.dto.CharacterMarvelDto
classMarvelCharacter(
valname:String,
valimageUrl:String
){
constructor(dto:CharacterMarvelDto):this(
name=dto.name,
imageUrl=dto.imageUrl
)
}
TherearedifferentwaystoprovideaninstanceofMarvelRepository.Inmostcommonimplementation,aconcreteinstanceofMarvelRepositoryispassedtoPresenterasconstructorargument.ButwhataboutUItesting(suchasEspresso
tests)?Wedon'twanttotesttheMarvelAPIandwedon'twanttomakeaUItestdependingonit.Thesolutionistomakeamechanismthatwillgeneratestandardimplementationduringnormalruntime,butitwillalsoallowustosetadifferentimplementationfortestingpurposes.Wewillmakethefollowinggenericimplementationofsuchmechanism(placeitindata):
packagecom.sample.marvelgallery.data
abstractclassProvider<T>{
abstractfuncreator():T
privatevalinstance:Tbylazy{creator()}
vartestingInstance:T?=null
funget():T=testingInstance?:instance
}
InsteadofdefiningourownProvider,wemightusesomeofDependencyInjectionlibraries,suchasDaggerorKodein.DaggerusageforsuchpurposesisreallycommoninAndroiddevelopment,butwe'vedecidedthatwewon'tincludeitinthisexampletoavoidadditionalcomplexityfordeveloperswhoarenotexperiencedwiththislibrary.
WecanmaketheMarvelRepositorycompanionobjectproviderextendaboveclass:
packagecom.sample.marvelgallery.data
importcom.sample.marvelgallery.model.MarvelCharacter
importio.reactivex.Single
interfaceMarvelRepository{
fungetAllCharacters():Single<List<MarvelCharacter>>
companionobject:Provider<MarvelRepository>(){
overridefuncreator()=MarvelRepositoryImpl()
}
}
Thankstotheprecedingdefinition,wecanusetheMarvelRepositorycompanionobjecttogetaninstanceofMarvelRepository:
valmarvelRepository=MarvelRepository.get()
ItwillbealazyinstanceofMarvelRepositoryImpl,untilsomebodysetssomenot-nullvalueofthetestingInstanceproperty:
MarvelRepository.get()//ReturnsinstanceofMarvelRepositoryImpl
MarvelRepository.testingInstance=object:MarvelRepository{
overridefungetAllCharacters():Single<List<MarvelCharacter>>
=Single.just(emptyList())
}
MarvelRepository.get()//returnsaninstanceofananonymousclassinwhichthereturnedlistisalwaysempty.
SuchaconstructionisusefultoallowUItestsusingespresso.ItsusageforelementoverrideispresentintheprojectandcanbefoundinGitHub.Itisnotpresentedinthissectiontokeepitsimplertounderstandfordeveloperswhoarenotproficientintesting.Ifyouarewillingtoseeit,thenyoucanfinditathttps://github.com/MarcinMoskala/MarvelGallery/blob/master/app/src/androidTest/java/com/sample/marvelgallery/MainActivityTest.kt.
Let'sfinallyconnectthisrepositorywithviewbyimplementationofthebusinesslogicofthecharactergallerydisplay.
BusinesslogicimplementationWehavebothviewandrepositorypartsimplementedanditistimetofinallyimplementthebusinesslogic.Onthispoint,weneedonlytogetthecharacterlistanddisplayitonthelistwhentheuserentersthescreenorwhentheyrefreshit.WewillextractthesebusinesslogicrulesfromviewimplementationbyusinganarchitecturalpatternknownasModel-View-Presenter(MVP).Herearethesimplifiedrules:
Model:Thisisthelayerresponsibleformanagingdata.Model'sresponsibilitiesincludeusingAPIs,cachingdata,managingdatabases,andsoon.Presenter:Thepresenteristhemiddle-manbetweenModelandView,anditshouldincludeallyourpresentationlogic.Thepresenterisresponsibleforreactingtouserinteractions,usingandupdatingtheModelandtheView.View:ThisisresponsibleforpresentingdataandforwardinguserinteractioneventstothePresenter.
Inourimplementationofthispattern,wewilltreatActivityasaView,andforeachviewweneedtocreateapresenter.Itisgoodpracticetowriteunitteststocheckwhetherbusinesslogicrulesareimplementedcorrectly.Tomakeitsimple,weneedtohideActivitybehindaneasy-to-mockinterfacethatisrepresentingallpossiblePresenterinteractionwithview(Activity).Also,wearegoingtocreateallthedependencies(suchasMarvelRepository)inActivityanddeliverthemtothePresenterviatheconstructorasobjectshiddenbehindinterfaces(forexample,passMarvelRepositoryImplasMarvelRepository).
InPresenter,weneedtoimplementthefollowingbehaviors:
WhenthePresenteriswaitingforaresponse,loadinganimationisdisplayedAftertheViewhasbeencreated,alistofcharactersisloadedanddisplayedAftertherefreshmethodiscalled,alistofcharactersisloadedWhentheAPIreturnsalistofcharacters,itisdisplayedontheviewWhentheAPIreturnsanerror,itisdisplayedontheview
Aswecansee,thePresenterneedstogetbyconstructorbothViewandMarvelRepository,anditshouldspecifythemethodsthatwillbecalledwhentheviewiscreatedortheuserrequestlistisrefreshed:
packagecom.sample.marvelgallery.presenter
importcom.sample.marvelgallery.data.MarvelRepository
importcom.sample.marvelgallery.view.main.MainView
classMainPresenter(valview:MainView,valrepository:MarvelRepository){
funonViewCreated(){
}
funonRefresh(){
}
}
TheViewneedstospecifythemethodsusedtoshowthelistofcharacters,showerrorandshowprogressbarwhenViewisrefreshing(defineitinview/mainandmoveMainActivitytoview/main):
packagecom.sample.marvelgallery.view.main.main
importcom.sample.marvelgallery.model.MarvelCharacter
interfaceMainView{
varrefresh:Boolean
funshow(items:List<MarvelCharacter>)
funshowError(error:Throwable)
}
Beforeaddinglogictoapresenter,let'sdefinefirsttwounittests:
//testsourceset
packagecom.sample.marvelgallery
importcom.sample.marvelgallery.data.MarvelRepository
importcom.sample.marvelgallery.model.MarvelCharacter
importcom.sample.marvelgallery.presenter.MainPresenter
importcom.sample.marvelgallery.view.main.MainView
importio.reactivex.Single
importorg.junit.Assert.assertEquals
importorg.junit.Assert.fail
importorg.junit.Test
@Suppress("IllegalIdentifier")//1
classMainPresenterTest{
@Test
fun`Afterviewwascreated,listofcharactersisloadedanddisplayed`(){
assertOnAction{onViewCreated()}.thereIsSameListDisplayed()
}
@Test
fun`Newlistisshownafterviewwasrefreshed`(){
assertOnAction{onRefresh()}.thereIsSameListDisplayed()
}
privatefunassertOnAction(action:MainPresenter.()->Unit)
=PresenterActionAssertion(action)
privateclassPresenterActionAssertion
(valactionOnPresenter:MainPresenter.()->Unit){
funthereIsSameListDisplayed(){
//Given
valexampleCharacterList=listOf(//2
MarvelCharacter("ExampleName","ExampleImageUrl"),
MarvelCharacter("Name1","ImageUrl1"),
MarvelCharacter("Name2","ImageUrl2")
)
vardisplayedList:List<MarvelCharacter>?=null
valview=object:MainView{//3
overridevarrefresh:Boolean=false
overridefunshow(items:List<MarvelCharacter>){
displayedList=items//4
}
overridefunshowError(error:Throwable){
fail()//5
}
}
valmarvelRepository=object:MarvelRepository{//3
overridefungetAllCharacters():
Single<List<MarvelCharacter>>
=Single.just(exampleCharacterList)//6
}
valmainPresenter=MainPresenter(view,marvelRepository)
//3
//When
mainPresenter.actionOnPresenter()//7
//Then
assertEquals(exampleCharacterList,displayedList)//8
}
}
}
1. DescriptivenamesareallowedinKotlinunittests,buttherewillbeawarningdisplayed.Thissuppressionisneededtohidethiswarning.
2. Definealistofexamplecharacterstodisplay.3. Defineaviewandrepositoryandcreateapresenterusingthem.4. Whenalistofelementsisshown,thenweshouldsetitasadisplayedlist.5. ThetestisfailingwhenshowErroriscalled.6. ThegetAllCharactersmethodisjustreturninganexamplelist.7. Wecalladefinedactiononthepresenter.8. Wecheckwhetherthelistreturnedbytherepositoryisthesameasthe
displayedlist.
Tosimplifytheprecedingdefinitions,wecouldextractBaseMarvelRepositoryandBaseMainView,andkeepexampledatainaseparateclass:
//testsourceset
packagecom.sample.marvelgallery.helpers
importcom.sample.marvelgallery.data.MarvelRepository
importcom.sample.marvelgallery.model.MarvelCharacter
importio.reactivex.Single
classBaseMarvelRepository(
valonGetCharacters:()->Single<List<MarvelCharacter>>
):MarvelRepository{
overridefungetAllCharacters()=onGetCharacters()
}
//testsourceset
packagecom.sample.marvelgallery.helpers
importcom.sample.marvelgallery.model.MarvelCharacter
importcom.sample.marvelgallery.view.main.MainView
classBaseMainView(
varonShow:(items:List<MarvelCharacter>)->Unit={},
valonShowError:(error:Throwable)->Unit={},
overridevarrefresh:Boolean=false
):MainView{
overridefunshow(items:List<MarvelCharacter>){
onShow(items)
}
overridefunshowError(error:Throwable){
onShowError(error)
}
}
//testsourceset
packagecom.sample.marvelgallery.helpers
importcom.sample.marvelgallery.model.MarvelCharacter
objectExample{
valexampleCharacter=MarvelCharacter
("ExampleName","ExampleImageUrl")
valexampleCharacterList=listOf(
exampleCharacter,
MarvelCharacter("Name1","ImageUrl1"),
MarvelCharacter("Name2","ImageUrl2")
)
}
NowwecansimplifythedefinitionofPresenterActionAssertion:
packagecom.sample.marvelgallery
importcom.sample.marvelgallery.helpers.BaseMainView
importcom.sample.marvelgallery.helpers.BaseMarvelRepository
importcom.sample.marvelgallery.helpers.Example
importcom.sample.marvelgallery.model.MarvelCharacter
importcom.sample.marvelgallery.presenter.MainPresenter
importio.reactivex.Single
importorg.junit.Assert.assertEquals
importorg.junit.Assert.fail
importorg.junit.Test
@Suppress("IllegalIdentifier")
classMainPresenterTest{
@Test
fun`Afterviewwascreated,listofcharactersisloadedanddisplayed`(){
assertOnAction{onViewCreated()}.thereIsSameListDisplayed()
}
@Test
fun`Newlistisshownafterviewwasrefreshed`(){
assertOnAction{onRefresh()}.thereIsSameListDisplayed()
}
privatefunassertOnAction(action:MainPresenter.()->Unit)
=PresenterActionAssertion(action)
privateclassPresenterActionAssertion
(valactionOnPresenter:MainPresenter.()->Unit){
funthereIsSameListDisplayed(){
//Given
vardisplayedList:List<MarvelCharacter>?=null
valview=BaseMainView(
onShow={items->displayedList=items},
onShowError={fail()}
)
valmarvelRepository=BaseMarvelRepository(
onGetCharacters=
{Single.just(Example.exampleCharacterList)}
)
valmainPresenter=MainPresenter(view,marvelRepository)
//When
mainPresenter.actionOnPresenter()
//Then
assertEquals(Example.exampleCharacterList,displayedList)
}
}
}
Westartthetests:
Wewillseethattheyarenotpassing:
ThereasonisthatfunctionalitiesarenotimplementedyetinMainPresenter.Thesimplestcodethatissatisfactorytopassthisunittestisthefollowing:
packagecom.sample.marvelgallery.presenter
importcom.sample.marvelgallery.data.MarvelRepository
importcom.sample.marvelgallery.view.main.MainView
classMainPresenter(valview:MainView,valrepository:MarvelRepository){
funonViewCreated(){
loadCharacters()
}
funonRefresh(){
loadCharacters()
}
privatefunloadCharacters(){
repository.getAllCharacters()
.subscribe({items->
view.show(items)
})
}
}
Nowourtestsarepassing:
Buttherearetwoissueswithfollowingimplementation:
Itwon'tworkinAndroid,becausegetAllCharactersisusinganetworkoperationanditcannotrunonthemainthreadasinthisexampleWewillhaveamemoryleakiftheuserlefttheapplicationbeforeloadinghadbeenfinished
Toresolvethefirstissue,weneedtospecifyonwhichthreadswhatoperationsshouldrun.ThenetworkrequestshouldberunningontheI/Othread,andweshouldobserveontheAndroidmainthread(becausewearechangingtheviewincallback):
repository.getAllCharacters()
.subscribeOn(Schedulers.io())//1
.observeOn(AndroidSchedulers.mainThread())//2
.subscribe({items->view.show(items)})
1. WespecifythatthenetworkrequestshouldberunninginIOthread.2. Wespecifythatcallbacksshouldbestartedonthemainthread.
Whilethesearecommonschedulerstoshow,wecanextracttheminatop-levelextensionfunction:
//RxExt.kt
packagecom.sample.marvelgallery.data
importio.reactivex.Single
importio.reactivex.android.schedulers.AndroidSchedulers
importio.reactivex.schedulers.Schedulers
fun<T>Single<T>.applySchedulers():Single<T>=this
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
AnduseitinMainPresenter:
repository.getAllCharacters()
.applySchedulers()
.subscribe({items->view.show(items)})
TestsarenotallowedtoaccesstheAndroidmainthread.Therefore,ourtestswillnotpass.Also,operationsrunningonanewthreadarenotwhatwewantinunittests,becausewewouldhaveproblemassertionssynchronization.Toresolvetheseproblems,weneedoverrideschedulersbeforeunitteststomakeeverythingrunonthesamethread(additinMainPresenterTestclass):
packagecom.sample.marvelgallery
importcom.sample.marvelgallery.helpers.BaseMainView
importcom.sample.marvelgallery.helpers.BaseMarvelRepository
importcom.sample.marvelgallery.helpers.Example
importcom.sample.marvelgallery.model.MarvelCharacter
importcom.sample.marvelgallery.presenter.MainPresenter
importio.reactivex.Single
importio.reactivex.android.plugins.RxAndroidPlugins
importio.reactivex.plugins.RxJavaPlugins
importio.reactivex.schedulers.Schedulers
importorg.junit.Assert.assertEquals
importorg.junit.Assert.fail
importorg.junit.Before
importorg.junit.Test
@Suppress("IllegalIdentifier")
classMainPresenterTest{
@Before
funsetUp(){
RxAndroidPlugins.setInitMainThreadSchedulerHandler{
Schedulers.trampoline()}
RxJavaPlugins.setIoSchedulerHandler{Schedulers.trampoline()}
RxJavaPlugins.setComputationSchedulerHandler{
Schedulers.trampoline()}
RxJavaPlugins.setNewThreadSchedulerHandler{
Schedulers.trampoline()}
}
@Test
fun`Afterviewwascreated,listofcharactersisloadedand
displayed`(){
assertOnAction{onViewCreated()}.thereIsSameListDisplayed()
}
@Test
fun`Newlistisshownafterviewwasrefreshed`(){
assertOnAction{onRefresh()}.thereIsSameListDisplayed()
}
Nowunittestsarepassingagain:
Anotherproblemismemoryleakiftheuserleavestheapplicationbeforewegetaserverresponse.Acommonsolutionistokeepallsubscriptionsincomposite,anddisposethemallwhentheuserisleavingtheapplication:
privatevarsubscriptions=CompositeDisposable()
funonViewDestroyed(){
subscriptions.dispose()
}
Inbiggerapplications,mostpresentershavesomesubscriptions.Sothe
functionalityofcollectingsubscriptionsanddisposingthemwhentheuserdestroystheviewcanbetreatedascommonbehaviorandextractedinBasePresenter.Also,tosimplifytheprocess,wecanmakeaBaseActivityWithPresenterclassthatwillholdthepresenterbehindthePresenterinterfaceandcalltheonViewDestroyedmethodwhentheviewisdestroyed.Let'sdefinethismechanisminourapplication.HereisthedefinitionofPresenter:
packagecom.sample.marvelgallery.presenter
interfacePresenter{
funonViewDestroyed()
}
HereisthedefinitionofBasePresenter:
packagecom.sample.marvelgallery.presenter
importio.reactivex.disposables.CompositeDisposable
abstractclassBasePresenter:Presenter{
protectedvarsubscriptions=CompositeDisposable()
overridefunonViewDestroyed(){
subscriptions.dispose()
}
}
HereisthedefinitionofBaseActivityWithPresenter:
packagecom.sample.marvelgallery.view.common
importandroid.support.v7.app.AppCompatActivity
importcom.sample.marvelgallery.presenter.Presenter
abstractclassBaseActivityWithPresenter:AppCompatActivity(){
abstractvalpresenter:Presenter
overridefunonDestroy(){
super.onDestroy()
presenter.onViewDestroyed()
}
}
Tosimplifyhowanewsubscriptionisaddedtosubscriptions,wecandefineaplusassignoperator:
//RxExt.ext
packagecom.sample.marvelgallery.data
importio.reactivex.Single
importio.reactivex.android.schedulers.AndroidSchedulers
importio.reactivex.disposables.CompositeDisposable
importio.reactivex.disposables.Disposable
importio.reactivex.schedulers.Schedulers
fun<T>Single<T>.applySchedulers():Single<T>=this
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
operatorfunCompositeDisposable.plusAssign(disposable:Disposable){
add(disposable)
}
AndwecanusebothsolutionstomakeMainPresentersecure:
packagecom.sample.marvelgallery.presenter
importcom.sample.marvelgallery.data.MarvelRepository
importcom.sample.marvelgallery.data.applySchedulers
importcom.sample.marvelgallery.data.plusAssign
importcom.sample.marvelgallery.view.main.MainView
classMainPresenter(
valview:MainView,
valrepository:MarvelRepository
):BasePresenter(){
funonViewCreated(){
loadCharacters()
}
funonRefresh(){
loadCharacters()
}
privatefunloadCharacters(){
subscriptions+=repository.getAllCharacters()
.applySchedulers()
.subscribe({items->
view.show(items)
})
}
}
ThefirsttwoMainPresenterbehaviorshavebeenimplemented.Itistimetomoveontothenextone--whentheAPIreturnsanerror,itisdisplayedontheview.WecanaddthisrequirementasatestinMainPresenterTest:
@Test
fun`Newlistisshownafterviewwasrefreshed`(){
assertOnAction{onRefresh()}.thereIsSameListDisplayed()
}
@Test
fun`WhenAPIreturnserror,itisdisplayedonview`(){
//Given
valsomeError=Error()
varerrorDisplayed:Throwable?=null
valview=BaseMainView(
onShow={_->fail()},
onShowError={errorDisplayed=it}
)
valmarvelRepository=BaseMarvelRepository
{Single.error(someError)}
valmainPresenter=MainPresenter(view,marvelRepository)
//When
mainPresenter.onViewCreated()
//Then
assertEquals(someError,errorDisplayed)
}
privatefunassertOnAction(action:MainPresenter.()->Unit)
=PresenterActionAssertion(action)
AsimplechangethatwillmakethistestpassiserrorhandlerspecificationinthesubscribemethodinMainPresenter:
subscriptions+=repository.getAllCharacters()
.applySchedulers()
.subscribe({items->//onNext
view.show(items)
},{//onError
view.showError(it)
})
WhilesubscribeisJavamethod,wecannotusenamedargumentconvention.Suchinvocationisnotreallydescriptive.ThisiswhywearegoingtodefineintheRxExt.ktcustomsubscribemethodnamedsubscribeBy:
//Ext.kt
fun<T>Single<T>.applySchedulers():Single<T>=this
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
fun<T>Single<T>.subscribeBy(
onError:((Throwable)->Unit)?=null,
onSuccess:(T)->Unit
):Disposable=subscribe(onSuccess,{onError?.invoke(it)})
Andwewilluseitinsteadofsubscribe:
subscriptions+=repository.getAllCharacters()
.applySchedulers()
.subscribeBy(
onSuccess=view::show,
onError=view::showError
)
subscribeByinfullversiondefinedfordifferentRxJavatyped(suchasObservable,Flowable,andsoon)togetherwithlotsofotherusefulKotlinextensionstoRxJavacanbefoundinRxKotlinlibrary(https://github.com/ReactiveX/RxKotlin).
Toshowandhidelistloading,wewilldefineadditionallistenerstoeventsthat
arealwaysoccurringbeforeandafterprocessing:
subscriptions+=repository.getAllCharacters()
.applySchedulers()
.doOnSubscribe{view.refresh=true},}
onSuccess=view::show,
.doFinally{view.refresh=false}
.subscribeBy(
onSuccess=view::show,
onError=view::showError,
onFinish={view.refresh=false}
)
Andtestsarepassingagain:
Thesubscribemethodisbecominglessandlessreadable,butwewillresolvethisproblemtogetherwithanotherbusinessrule,whosedefinitionisthefollowing--whenthepresenteriswaitingforaresponse,refreshisdisplayed.DefineitsunittestinMainPresenterTest:
packagecom.sample.marvelgallery
importcom.sample.marvelgallery.helpers.BaseMainView
importcom.sample.marvelgallery.helpers.BaseMarvelRepository
importcom.sample.marvelgallery.helpers.Example
importcom.sample.marvelgallery.model.MarvelCharacter
importcom.sample.marvelgallery.presenter.MainPresenter
importio.reactivex.Single
importio.reactivex.android.plugins.RxAndroidPlugins
importio.reactivex.plugins.RxJavaPlugins
importio.reactivex.schedulers.Schedulers
importorg.junit.Assert.*
importorg.junit.Before
importorg.junit.Test
@Suppress("IllegalIdentifier")
classMainPresenterTest{
@Test
fun`Whenpresenteriswaitingforresponse,refreshisdisplayed`()
{
//Given
valview=BaseMainView(refresh=false)
valmarvelRepository=BaseMarvelRepository(
onGetCharacters={
Single.fromCallable{
//Then
assertTrue(view.refresh)//1
Example.exampleCharacterList
}
}
)
valmainPresenter=MainPresenter(view,marvelRepository)
view.onShow={_->
//Then
assertTrue(view.refresh)//1
}
//When
mainPresenter.onViewCreated()
//Then
assertFalse(view.refresh)//1
}
}
1. Weexpectrefreshdisplayedduringnetworkrequestandwhenelementsareshown,butnotafterprocessingfinishes.
Weexpectrefreshtobedisplayedduringanetworkrequestandwhenelementsareshown,butnotafterprocessingfinishes.
InthepresentedversiononRxJava2,assertionsinsidecallbacksarenotbreakingthetestbutdisplayinganerrorontheexecutionreportinstead:
Probably,infutureversions,itwillbepossibletoaddahandlerthatisallowingtofailatestfrominsideacallback.
Toshowandhidelistloading,wewilldefineadditionallistenerstoeventsthatarealwaysoccurringbeforeandafterprocessing:
subscriptions+=repository.getAllCharacters()
.applySchedulers()
.doOnSubscribe{view.refresh=true}
.doFinally{view.refresh=false}
.subscribeBy(
onSuccess=view::show,
onError=view::showError
)
Afterthesechanges,alltestsarepassingagain:
Nowwehaveafullyfunctionalpresenter,network,andview.Timetoconnectitallandfinishimplementationofthefirstusecase.
PuttingitalltogetherWehaveMainPresenterreadytobeusedintheproject.NowweneedtouseitinMainActivity:
packagecom.sample.marvelgallery.view.main
importandroid.os.Bundle
importandroid.support.v7.widget.GridLayoutManager
importandroid.view.Window
importcom.sample.marvelgallery.R
importcom.sample.marvelgallery.data.MarvelRepository
importcom.sample.marvelgallery.model.MarvelCharacter
importcom.sample.marvelgallery.presenter.MainPresenter
importcom.sample.marvelgallery.view.common.BaseActivityWithPresenter
importcom.sample.marvelgallery.view.common.bindToSwipeRefresh
importcom.sample.marvelgallery.view.common.toast
importkotlinx.android.synthetic.main.activity_main.*
classMainActivity:BaseActivityWithPresenter(),MainView{//1
overridevarrefreshbybindToSwipeRefresh(R.id.swipeRefreshView)
//2
overridevalpresenterbylazy
{MainPresenter(this,MarvelRepository.get())}//3
overridefunonCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
setContentView(R.layout.activity_main)
recyclerView.layoutManager=GridLayoutManager(this,2)
swipeRefreshView.setOnRefreshListener
{presenter.onRefresh()}//4
presenter.onViewCreated()//4
}
overridefunshow(items:List<MarvelCharacter>){
valcategoryItemAdapters=items.map(::CharacterItemAdapter)
recyclerView.adapter=MainListAdapter(categoryItemAdapters)
}
overridefunshowError(error:Throwable){
toast("Error:${error.message}")//2
error.printStackTrace()
}
}
1. ActivityshouldextendBaseActivityWithPresenterandimplementMainView.2. bindToSwipeRefreshandtoastarenotyetimplemented.3. Wemakepresenterlazily.Thefirstargumentisareferencetoactivity
behindtheMainViewinterface.4. Weneedtopasseventstothepresenterusingitsmethods.
Intheprecedingcode,weusedtwofunctionsthatwerealreadydescribedinthebook,toast,usedtodisplaytoastonthescreen,andbindToSwipeRefresh,usedtobindpropertywithvisibilityofswiperefresh:
//ViewExt.kt
packagecom.sample.marvelgallery.view.common
importandroid.app.Activity
importandroid.content.Context
importandroid.support.annotation.IdRes
importandroid.support.v4.widget.SwipeRefreshLayout
importandroid.support.v7.widget.RecyclerView
importandroid.view.View
importandroid.widget.ImageView
importandroid.widget.Toast
importcom.bumptech.glide.Glide
importkotlin.properties.ReadWriteProperty
importkotlin.reflect.KProperty
fun<T:View>RecyclerView.ViewHolder.bindView(viewId:Int)
=lazy{itemView.findViewById<T>(viewId)}
funImageView.loadImage(photoUrl:String){
Glide.with(context)
.load(photoUrl)
.into(this)
}
funContext.toast(text:String,length:Int=Toast.LENGTH_LONG){
Toast.makeText(this,text,length).show()
}
funActivity.bindToSwipeRefresh(@IdResswipeRefreshLayoutId:Int):ReadWriteProperty<Any?,Boolean>
=SwipeRefreshBinding(lazy{findViewById<SwipeRefreshLayout>(swipeRefreshLayoutId)})
privateclassSwipeRefreshBinding(lazyViewProvider:Lazy<SwipeRefreshLayout>):ReadWriteProperty<Any?,Boolean>{
valviewbylazyViewProvider
overridefungetValue(thisRef:Any?,
property:KProperty<*>):Boolean{
returnview.isRefreshing
}
overridefunsetValue(thisRef:Any?,
property:KProperty<*>,value:Boolean){
view.isRefreshing=value
}
}
Nowourapplicationshouldcorrectlyshowalistofcharacters:
Ourfirstusecasehasbeenimplemented.Wecanmoveontothenextone.
CharactersearchAnotherbehaviorweneedtoimplementischaractersearch.Hereistheusecasedefinition,afterstartingtheapplication,theusercansearchforacharacterbyitsname.
Toaddit,wearegoingtoaddEditTexttotheactivity_mainlayout:
<?xmlversion="1.0"encoding="utf-8"?>
<RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/charactersView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:fitsSystemWindows="true">
<!--DummyitemtopreventEditTextfromreceiving
focusoninitialload-->
<LinearLayout
android:layout_width="0px"
android:layout_height="0px"
android:focusable="true"
android:focusableInTouchMode="true"
tools:ignore="UselessLeaf"/>
<android.support.design.widget.TextInputLayout
android:id="@+id/searchViewLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/element_padding">
<EditText
android:id="@+id/searchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:hint="@string/search_hint"/>
</android.support.design.widget.TextInputLayout>
<android.support.v4.widget.SwipeRefreshLayoutxmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/swipeRefreshView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/searchViewLayout"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"/>
</android.support.v4.widget.SwipeRefreshLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="@android:color/white"
android:gravity="center"
android:text="@string/marvel_copyright_notice"/>
</RelativeLayout>
WeneedtoaddAndroidSupportDesignlibrarydependencytoallowTextInputLayoutusage:
implementation"com.android.support:appcompat-v7:$android_support_version"
implementation"com.android.support:design:$android_support_version"
implementation"com.android.support:recyclerview-v7:$android_support_version"
Andstringsearch_hintdefinitioninstrings.xml:
<resources>
<stringname="app_name">MarvelGallery</string>
<stringname="search_hint">Searchforcharacter</string>
<stringname="marvel_copyright_notice">
DataprovidedbyMarvel.©2017MARVEL
</string>
</resources>
Also,tokeepthelabelthatisinformingaboutMarvelcopyrightwhenthekeyboardisopened,wealsoneedtoadjustResizetowindowSoftInputModeinactivitydefinitioninAndroidManifest:
<activity
android:name="com.sample.marvelgallery.view.main.MainActivity"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<actionandroid:name="android.intent.action.MAIN"/>
<categoryandroid:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
Weshouldseethefollowingpreview:
NowwehaveasearchfieldaddedinMainActivity:
Thebehaviorweareexpectingisthatwhenevertheuserchangesthetextinthe
searchfield,anewlistwillbeloaded.WeneedanewmethodinMainPresenter,thatwillbeusedtoinformthepresenterthatthetextwaschanged.WewillcallitonSearchChanged:
funonRefresh(){
loadCharacters()
}
funonSearchChanged(text:String){
//TODO
}
privatefunloadCharacters(){
subscriptions+=repository.getAllCharacters()
.applySchedulers()
.doOnSubscribe{view.refresh=true}
.doFinally{view.refresh=false}
.subscribeBy(
onSuccess=view::show,
onError=view::showError
)
}
}
WeneedtochangetheMarvelRepositorydefinitiontoacceptasearchqueryasgetAllCharactersparameter(remembertoupdatealsoBaseMarvelRepository):
interfaceMarvelRepository{
fungetAllCharacters(searchQuery:String?):
Single<List<MarvelCharacter>>
companionobject:Provider<MarvelRepository>(){
overridefuncreator()=MarvelRepositoryImpl()
}
}
Asaresult,wehavetoupdatetheimplementation:
classMarvelRepositoryImpl:MarvelRepository{
valapi=retrofit.create(MarvelApi::class.java)
overridefungetAllCharacters(searchQuery:String?):
Single<List<MarvelCharacter>>=api.getCharacters(
offset=0,
searchQuery=searchQuery,
limit=elementsOnListLimit
).map{it.data?.results.orEmpty().map(::MarvelCharacter)?:
emptyList()}
companionobject{
constvalelementsOnListLimit=50
}
}
Wealsoupdatethenetworkrequestdefinition:
interfaceMarvelApi{
@GET("characters")
fungetCharacters(
@Query("offset")offset:Int?,
@Query("nameStartsWith")searchQuery:String?,
@Query("limit")limit:Int?
):Single<DataWrapper<List<CharacterMarvelDto>>>
}
Andtoallowcodecompilation,weneedtoprovidenullasagetAllCharactersargumentinMainPresenter:
privatefunloadCharacters(){
subscriptions+=repository.getAllCharacters(null)
.applySchedulers()
.doOnSubscribe{view.refresh=true}
.doFinally{view.refresh=false}
.subscribeBy(
onSuccess=view::show,
onError=view::showError
)
}
}
AndweneedtoupdateBaseMarvelRepository:
packagecom.sample.marvelgallery.helpers
importcom.sample.marvelgallery.data.MarvelRepository
importcom.sample.marvelgallery.model.MarvelCharacter
importio.reactivex.Single
classBaseMarvelRepository(
valonGetCharacters:(String?)->Single<List<MarvelCharacter>>
):MarvelRepository{
overridefungetAllCharacters(searchQuery:String?)
=onGetCharacters(searchQuery)
}
Nowournetworkimplementationisreturningalistofcharactersthatstartsfromaquery,orafilllistifwedon'tspecifyanyquery.Timetoimplementthepresenter.Let'sdefinethefollowingtests:
@file:Suppress("IllegalIdentifier")
packagecom.sample.marvelgallery
importcom.sample.marvelgallery.helpers.BaseMainView
importcom.sample.marvelgallery.helpers.BaseMarvelRepository
importcom.sample.marvelgallery.presenter.MainPresenter
importio.reactivex.Single
importorg.junit.Assert.*
importorg.junit.Test
classMainPresenterSearchTest{
@Test
fun`Whenviewiscreated,thensearchqueryisnull`(){
assertOnAction{onViewCreated()}searchQueryIsEqualTonull
}
@Test
fun`Whentextischanged,thenwearesearchingfornewquery`(){
for(textinlistOf("KKO","HJHJ","Andsowhat?"))
assertOnAction{onSearchChanged(text)}
searchQueryIsEqualTotext
}
privatefunassertOnAction(action:MainPresenter.()->Unit)
=PresenterActionAssertion(action)
privateclassPresenterActionAssertion(valactionOnPresenter:
MainPresenter.()->Unit){
infixfunsearchQueryIsEqualTo(expectedQuery:String?){
varcheckApplied=false
valview=BaseMainView(onShowError={fail()})
valmarvelRepository=BaseMarvelRepository{searchQuery->
assertEquals(expectedQuery,searchQuery)
checkApplied=true
Single.never()
}
valmainPresenter=MainPresenter(view,marvelRepository)
mainPresenter.actionOnPresenter()
assertTrue(checkApplied)
}
}
}
Tomakefollowingtestpass,weneedtoaddsearchqueryasaparameterwithdefaultargumenttotheloadCharactersmethodofMainPresenter:
funonSearchChanged(text:String){
loadCharacters(text)
}
privatefunloadCharacters(searchQuery:String?=null){
subscriptions+=repository.getAllCharacters(searchQuery)
.applySchedulers()
.doOnSubscribe{view.refresh=true}
.doFinally{view.refresh=false}
.subscribeBy(
onSuccess=view::show,
onError=view::showError
)
}
}
ButthetrickypartisthattheMarvelAPIdoesnotallowonlywhitespacesasansearchquery.Thereshouldbeanullsendinstead.Therefore,iftheuserdeletesthelastcharacteroriftheytrytosearchplaceonlyspaceinsearchfield,thenthe
applicationwouldcrash.Weshouldpreventsuchsituations.Hereisatestthatischeckingwhetherthepresenterischangingaquerywithonlywhitespacesintonull:
@Test
fun`Whentextischanged,thenwearesearchingfornewquery`(){
for(textinlistOf("KKO","HJHJ","Andsowhat?"))
assertOnAction{onSearchChanged(text)}
searchQueryIsEqualTotext
}
@Test
fun`Forblanktext,thereisrequestwithnullquery`(){
for(emptyTextinlistOf("","",""))
assertOnAction{onSearchChanged(emptyText)}
searchQueryIsEqualTonull
}
privatefunassertOnAction(action:MainPresenter.()->Unit)
=PresenterActionAssertion(action)
WecanimplementasecuritymechanismintheloadCharactersmethod:
privatefunloadCharacters(searchQuery:String?=null){
valqualifiedSearchQuery=if(searchQuery.isNullOrBlank())null
elsesearchQuery
subscriptions+=repository
.getAllCharacters(qualifiedSearchQuery)
.applySchedulers()
.smartSubscribe(
onStart={view.refresh=true},
onSuccess=view::show,
onError=view::showError,
onFinish={view.refresh=false}
)
}
Nowalltestsarepassingagain:
WestillneedtoimplementanActivityfunctionalitythatwillcallthepresenterwhentexthaschanged.WewilldoitusingtheoptionalcallbackclassdefinedinChapter7,ExtensionFunctionsandProperties:
//TextChangedListener.kt
packagecom.sample.marvelgallery.view.common
importandroid.text.Editable
importandroid.text.TextWatcher
importandroid.widget.TextView
funTextView.addOnTextChangedListener(config:TextWatcherConfiguration.()->Unit){
addTextChangedListener(TextWatcherConfiguration().apply{config()}
addTextChangedListener(textWatcher)
}
classTextWatcherConfiguration:TextWatcher{
privatevarbeforeTextChangedCallback:
(BeforeTextChangedFunction)?=null
privatevaronTextChangedCallback:
(OnTextChangedFunction)?=null
privatevarafterTextChangedCallback:
(AfterTextChangedFunction)?=null
funbeforeTextChanged(callback:BeforeTextChangedFunction){
beforeTextChangedCallback=callback
}
funonTextChanged(callback:OnTextChangedFunction){
onTextChangedCallback=callback
}
funafterTextChanged(callback:AfterTextChangedFunction){
afterTextChangedCallback=callback
}
overridefunbeforeTextChanged(s:CharSequence,
start:Int,count:Int,after:Int){
beforeTextChangedCallback?.invoke(s.toString(),
start,count,after)
}
overridefunonTextChanged(s:CharSequence,start:Int,
before:Int,count:Int){
onTextChangedCallback?.invoke(s.toString(),
start,before,count)
}
overridefunafterTextChanged(s:Editable){
afterTextChangedCallback?.invoke(s)
}
}
privatetypealiasBeforeTextChangedFunction=
(text:String,start:Int,count:Int,after:Int)->Unit
privatetypealiasOnTextChangedFunction=
(text:String,start:Int,before:Int,count:Int)->Unit
privatetypealiasAfterTextChangedFunction=
(s:Editable)->Unit
AnduseitintheonCreatemethodofMainActivity:
packagecom.sample.marvelgallery.view.main
importandroid.os.Bundle
importandroid.support.v7.widget.GridLayoutManager
importandroid.view.Window
importcom.sample.marvelgallery.R
importcom.sample.marvelgallery.data.MarvelRepository
importcom.sample.marvelgallery.model.MarvelCharacter
importcom.sample.marvelgallery.presenter.MainPresenter
importcom.sample.marvelgallery.view.common.BaseActivityWithPresenter
importcom.sample.marvelgallery.view.common.addOnTextChangedListener
importcom.sample.marvelgallery.view.common.bindToSwipeRefresh
importcom.sample.marvelgallery.view.common.toast
importkotlinx.android.synthetic.main.activity_main.*
classMainActivity:BaseActivityWithPresenter(),MainView{
overridevarrefreshbybindToSwipeRefresh(R.id.swipeRefreshView)
overridevalpresenterbylazy
{MainPresenter(this,MarvelRepository.get())}
overridefunonCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
setContentView(R.layout.activity_main)
recyclerView.layoutManager=GridLayoutManager(this,2)
swipeRefreshView.setOnRefreshListener{presenter.onRefresh()}
searchView.addOnTextChangedListener{
onTextChanged{text,_,_,_->
presenter.onSearchChanged(text)
}
}
presenter.onViewCreated()
}
overridefunshow(items:List<MarvelCharacter>){
valcategoryItemAdapters=items.map(::CharacterItemAdapter)
recyclerView.adapter=MainListAdapter(categoryItemAdapters)
}
overridefunshowError(error:Throwable){
toast("Error:${error.message}")
error.printStackTrace()
}
}
Thatisallweneedtodefinethefunctionalityofthecharactersearch.Nowwecanbuildtheapplicationanduseittofindourfavoritecharacter:
Withacorrectlyworkingapplication,wecanmoveontothenextusecase.
CharacterprofiledisplaySearchingthroughcharactersisnotenough.Tomaketheappfunctional,weshouldaddacharacterdescriptiondisplay.Hereistheusecasewe'vedefined--whentheuserclicksonsomecharacterpicture,thereisaprofiledisplayed.Thecharacterprofilecontainscharactername,photo,description,anditsoccurrences.
Toimplementthisusecase,weneedtocreateanewactivityandlayoutthatwilldefinewhatthisActivitylookslike.Todoit,createanewActivitycalledCharacterProfileActivityinthepackagecom.sample.marvelgallery.view.character:
Wewillstartitsimplementationfromchangesinlayout(inactivity_character_profile.xml).Hereisthefinalresultwewouldliketoachieve:
ThebaseelementisCoordinatorLayoutwithAppBarandCollapsingToolbarLayoutbothusedtoachieveacollapsingeffectknownfrommaterialdesign:
Collapsingeffectstepbystep.
WealsoneedTextViewfordescriptionandoccurrencesthatwillbefilledwithdatainthenextusecase.Hereisthefullactivity_character_profilelayoutdefinition:
<?xmlversion="1.0"encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayoutxmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/character_detail_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white">
<android.support.design.widget.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.ActionBar">
<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/toolbarLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:contentScrim="?attr/colorPrimary"
app:expandedTitleTextAppearance="@style/ItemTitleName"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<android.support.v7.widget.AppCompatImageView
android:id="@+id/headerView"
android:layout_width="match_parent"
android:layout_height="@dimen/character_header_height"
android:background="@color/colorPrimaryDark"
app:layout_collapseMode="parallax"/>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@android:color/transparent"
app:layout_collapseMode="pin"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:id="@+id/details_content_frame"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusableInTouchMode="true"
android:orientation="vertical">
<TextView
android:id="@+id/descriptionView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="@dimen/character_description_padding"
android:textSize="@dimen/standard_text_size"
tools:text="Thisissomelongtextthatwillbevisibleasancharacterdescription."/>
<TextView
android:id="@+id/occurrencesView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/character_description_padding"
android:textSize="@dimen/standard_text_size"
tools:text="Hewasinfollowingcomics:\n*KOKOKO\n*KOKOKO\n*KOKOKO\n*KOKOKO\n*KOKOKO\n*KOKOKO\n*KOKOKO\n*KOKOKO\n*KOKOKO\n*KOKOKO\n*KOKOKO"/>
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@android:color/white"
android:gravity="bottom|center"
android:text="@string/marvel_copyright_notice"/>
<ProgressBar
android:id="@+id/progressView"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"/>
</android.support.design.widget.CoordinatorLayout>
Wealsoneedtoaddfollowingstylesinstyles.xml:
<resources>
<!--Baseapplicationtheme.-->
<stylename="AppTheme"
parent="Theme.AppCompat.Light.DarkActionBar">
<!--Customizeyourthemehere.-->
<itemname="colorPrimary">@color/colorPrimary</item>
<itemname="colorPrimaryDark">@color/colorPrimaryDark</item>
<itemname="colorAccent">@color/colorAccent</item>
</style>
<stylename="AppFullScreenTheme"
parent="Theme.AppCompat.Light.NoActionBar">
<itemname="android:windowNoTitle">true</item>
<itemname="android:windowActionBar">false</item>
<itemname="android:windowFullscreen">true</item>
<itemname="android:windowContentOverlay">@null</item>
</style>
<stylename="ItemTitleName"
parent="TextAppearance.AppCompat.Headline">
<itemname="android:textColor">@android:color/white</item>
<itemname="android:shadowColor">@color/colorPrimaryDark</item>
<itemname="android:shadowRadius">3.0</item>
</style>
<stylename="ItemDetailTitle"
parent="@style/TextAppearance.AppCompat.Small">
<itemname="android:textColor">@color/colorAccent</item>
</style>
</resources>
AndweneedtodefineAppFullScreenThemeasthethemeforCharacterProfileActivityinAndroidManifest:
<activityandroid:name=".view.CharacterProfileActivity"
android:theme="@style/AppFullScreenTheme"/>
Hereisapreviewofthedefinedlayout:
Thisviewwillbeusedtodisplaydataaboutthecharacter,butfirstweneedtoopenitfromMainActivity.WeneedtosetonClickListenerinCharacterItemAdapter,thatiscallingclickedcallbackprovidedbyconstructor:
packagecom.sample.marvelgallery.view.main
importandroid.support.v7.widget.RecyclerView
importandroid.view.View
importandroid.widget.ImageView
importandroid.widget.TextView
importcom.sample.marvelgallery.R
importcom.sample.marvelgallery.model.MarvelCharacter
importcom.sample.marvelgallery.view.common.ItemAdapter
importcom.sample.marvelgallery.view.common.bindView
importcom.sample.marvelgallery.view.common.loadImage
classCharacterItemAdapter(
valcharacter:MarvelCharacter,
valclicked:(MarvelCharacter)->Unit
):ItemAdapter<CharacterItemAdapter.ViewHolder>(R.layout.item_character){
overridefunonCreateViewHolder(itemView:View)=
ViewHolder(itemView)
overridefunViewHolder.onBindViewHolder(){
textView.text=character.name
imageView.loadImage(character.imageUrl)
itemView.setOnClickListener{clicked(character)}
}
classViewHolder(itemView:View):
RecyclerView.ViewHolder(itemView){
valtextViewbybindView<TextView>(R.id.textView)
valimageViewbybindView<ImageView>(R.id.imageView)
}
}
AndweneedtoupdateMainActivity:
packagecom.sample.marvelgallery.view.main
importandroid.os.Bundle
importandroid.support.v7.widget.GridLayoutManager
importandroid.view.Window
importcom.sample.marvelgallery.R
importcom.sample.marvelgallery.data.MarvelRepository
importcom.sample.marvelgallery.model.MarvelCharacter
importcom.sample.marvelgallery.presenter.MainPresenter
importcom.sample.marvelgallery.view.character.CharacterProfileActivity
importcom.sample.marvelgallery.view.common.BaseActivityWithPresenter
importcom.sample.marvelgallery.view.common.addOnTextChangedListener
importcom.sample.marvelgallery.view.common.bindToSwipeRefresh
importcom.sample.marvelgallery.view.common.toast
importkotlinx.android.synthetic.main.activity_main.*
classMainActivity:BaseActivityWithPresenter(),MainView{
overridevarrefreshbybindToSwipeRefresh(R.id.swipeRefreshView)
overridevalpresenterbylazy
{MainPresenter(this,MarvelRepository.get())}
overridefunonCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
setContentView(R.layout.activity_main)
recyclerView.layoutManager=GridLayoutManager(this,2)
swipeRefreshView.setOnRefreshListener{presenter.onRefresh()}
searchView.addOnTextChangedListener{
onTextChanged{text,_,_,_->
presenter.onSearchChanged(text)
}
}
presenter.onViewCreated()
}
overridefunshow(items:List<MarvelCharacter>){
valcategoryItemAdapters=
items.map(this::createCategoryItemAdapter)
recyclerView.adapter=MainListAdapter(categoryItemAdapters)
}
overridefunshowError(error:Throwable){
toast("Error:${error.message}")
error.printStackTrace()
}
privatefuncreateCategoryItemAdapter(character:MarvelCharacter)
=CharacterItemAdapter(character,
{showHeroProfile(character)})
privatefunshowHeroProfile(character:MarvelCharacter){
CharacterProfileActivity.start(this,character)
}
}
Intheprecedingimplementation,weareusingamethodfromtheCharacterProfileActivitycompanionobjecttostartCharacterProfileActivity.WeneedtopasstheMarvelCharacterobjecttothismethod.ThemostefficientwaytopassaMarvelCharacterobjectispassitasparcelable.Toallowit,MarvelCharactermustimplementtheParcelableinterface.ThisiswhyausefulsolutionistousesomeannotationprocessinglibrarysuchasParceler,PaperParcel,orSmuggler,thatgeneratesthenecessaryelements.WewillusesolutionfromKotlinAndroidextensionswealreadyhaveintheproject.Duringbookpublication,itwasstillexperimental,sothereneedstobeaddedfollowingdefinitioninthebuild.gradlemodule:
androidExtensions{
experimental=true
}
AllweneedtodoittoaddParcelizeannotationbeforeclass,andweneedtomakethisclassimplementParcelable.WewillalsoadderrorsuppressionbecausetohidedefaultAndroidwarning:
packagecom.sample.marvelgallery.model
importandroid.annotation.SuppressLint
importandroid.os.Parcelable
importcom.sample.marvelgallery.data.network.dto.CharacterMarvelDto
importkotlinx.android.parcel.Parcelize
@SuppressLint("ParcelCreator")
@Parcelize
constructor(dto:CharacterMarvelDto):this(
name=dto.name,
imageUrl=dto.imageUrl
)
}
Nowwecanimplementthestartfunctionandfieldcharacter,thatwillgettheargumentvaluefromIntentusingthepropertydelegate:
packagecom.sample.marvelgallery.view.character
importandroid.content.Context
importandroid.support.v7.app.AppCompatActivity
importandroid.os.Bundle
importandroid.view.MenuItem
importcom.sample.marvelgallery.R
importcom.sample.marvelgallery.model.MarvelCharacter
importcom.sample.marvelgallery.view.common.extra
importcom.sample.marvelgallery.view.common.getIntent
importcom.sample.marvelgallery.view.common.loadImage
importkotlinx.android.synthetic.main.activity_character_profile.*
classCharacterProfileActivity:AppCompatActivity(){
valcharacter:MarvelCharacterbyextra(CHARACTER_ARG)//1
overridefunonCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_character_profile)
setUpToolbar()
supportActionBar?.title=character.name
headerView.loadImage(character.imageUrl,centerCropped=true)//1
}
overridefunonOptionsItemSelected(item:MenuItem):Boolean=when{
item.itemId==android.R.id.home->onBackPressed().let{true}
else->super.onOptionsItemSelected(item)
}
privatefunsetUpToolbar(){
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
companionobject{
privateconstvalCHARACTER_ARG="com.sample.marvelgallery.view.character.CharacterProfileActivity.CharacterArgKey"
funstart(context:Context,character:MarvelCharacter){
valintent=context
.getIntent<CharacterProfileActivity>()//1
.apply{putExtra(CHARACTER_ARG,character)}
context.startActivity(intent)
}
}
}
1. TheextraandgetIntentextensionfunctionswerealreadypresentedinthebook,buttheyarenotimplementedyetintheproject.Also,loadImagewilldisplayanerrorbecauseitneedstobechanged.
WeneedtoupdateloadImage,anddefineextraandgetIntentastop-levelfunctions:
//ViewExt.kt
packagecom.sample.marvelgallery.view.common
importandroid.app.Activity
importandroid.content.Context
importandroid.content.Intent
importandroid.os.Parcelable
importandroid.support.annotation.IdRes
importandroid.support.v4.widget.SwipeRefreshLayout
importandroid.widget.ImageView
importandroid.widget.Toast
importcom.bumptech.glide.Glide
importkotlin.properties.ReadWriteProperty
importkotlin.reflect.KProperty
importandroid.support.v7.widget.RecyclerView
importandroid.view.View
fun<T:View>RecyclerView.ViewHolder.bindView(viewId:Int)
=lazy{itemView.findViewById<T>(viewId)}
funImageView.loadImage(photoUrl:String,centerCropped:Boolean=false){
Glide.with(context)
.load(photoUrl)
.apply{if(centerCropped)centerCrop()}
.into(this)
}
fun<T:Parcelable>Activity.extra(key:String,default:T?=null):Lazy<T>
=lazy{intent?.extras?.getParcelable<T>(key)?:default?:throwError("Novalue$keyinextras")}
inlinefun<reifiedT:Activity>Context.getIntent()=Intent(this,T::class.java)
//...
InsteadofdefiningfunctionstostarttheActivity,wemightusesomelibrarythatisgeneratingthesemethods.Forexample,wemightusetheActivityStarterlibrary.ThisiswhatCharacterProfileActivitywouldlooklike:
classCharacterProfileActivity:AppCompatActivity(){
@get:Argvalcharacter:MarvelCharacterbyargExtra()
overridefunonCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_character_profile)
setUpToolbar()
supportActionBar?.title=character.name
headerView.loadImage(character.imageUrl,centerCropped=true)//1
}
overridefunonOptionsItemSelected(item:MenuItem):Boolean=when{
item.itemId==android.R.id.home->onBackPressed().let{true}
else->super.onOptionsItemSelected(item)
}
privatefunsetUpToolbar(){
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
}
WeshouldstartitofgetitsintentusingstaticmethodsofthegeneratedclassCharacterProfileActivityStarter:
CharacterProfileActivityStarter.start(context,character)
valintent=CharacterProfileActivityStarter.getIntent(context,character)
Toallowit,weneedthekaptplugininthemodulebuild.gradle(usedtosupportannotationprocessinginKotlin):
applyplugin:'kotlin-kapt'
AndActivityStarterdependenciesinmodulebuild.gradle:
implementation'com.github.marcinmoskala.activitystarter:activitystarter:1.00'
implementation'com.github.marcinmoskala.activitystarter:activitystarter-kotlin:1.00'
kapt'com.github.marcinmoskala.activitystarter:activitystarter-compiler:1.00'
Afterthesechanges,whenweclickintocharacterinMainActivity,thenCharacterProfileActivitywillbestarted:
Wearedisplayingthenameandshowingthecharacterphoto.Thenextstepistodisplaythedescriptionandlistofoccurrences.ThenecessarydatacanbefoundintheMarvelAPIandweonlyneedtoextendDTOmodelstogetthem.WeneedtoaddListWrapperthatisusedtoholdalist:
packagecom.sample.marvelgallery.data.network.dto
classListWrapper<T>{
varitems:List<T>=listOf()
}
WeneedtodefineComicDto,whichholdsthedataweneedaboutoccurrence:
packagecom.sample.marvelgallery.data.network.dto
classComicDto{
lateinitvarname:String
}
AndweneedtoupdateCharacterMarvelDto:
packagecom.sample.marvelgallery.data.network.dto
classCharacterMarvelDto{
lateinitvarname:String
lateinitvardescription:String
lateinitvarthumbnail:ImageDto
varcomics:ListWrapper<ComicDto>=ListWrapper()
varseries:ListWrapper<ComicDto>=ListWrapper()
varstories:ListWrapper<ComicDto>=ListWrapper()
varevents:ListWrapper<ComicDto>=ListWrapper()
valimageUrl:String
get()=thumbnail.completeImagePath
}
DataisnowreadfromtheAPIandkeptinDTOobjects,buttousethemintheproject,wealsoneedtochangetheMarvelCharacterclassdefinition,andaddanewconstructor:
@SuppressLint("ParcelCreator")
@Parcelize
classMarvelCharacter(
valname:String,
valimageUrl:String,
valdescription:String,
valcomics:List<String>,
valseries:List<String>,
valstories:List<String>,
valevents:List<String>
):Parcelable{
constructor(dto:CharacterMarvelDto):this(
name=dto.name,
imageUrl=dto.imageUrl,
description=dto.description,
comics=dto.comics.items.map{it.name},
series=dto.series.items.map{it.name},
stories=dto.stories.items.map{it.name},
events=dto.events.items.map{it.name}
)
}
NowwecanupdateCharacterProfileActivitytodisplaythedescriptionandlistofoccurrences:
classCharacterProfileActivity:AppCompatActivity(){
valcharacter:MarvelCharacterbyextra(CHARACTER_ARG)
overridefunonCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_character_profile)
setUpToolbar()
supportActionBar?.title=character.name
descriptionView.text=character.description
occurrencesView.text=makeOccurrencesText()//1
headerView.loadImage(character.imageUrl,centerCropped=true)
}
overridefunonOptionsItemSelected(item:MenuItem):Boolean=when{
item.itemId==android.R.id.home->onBackPressed().let{true}
else->super.onOptionsItemSelected(item)
}
privatefunsetUpToolbar(){
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
privatefunmakeOccurrencesText():String=""//1,2
.addList(R.string.occurrences_comics_list_introduction,character.comics)
.addList(R.string.occurrences_series_list_introduction,character.series)
.addList(R.string.occurrences_stories_list_introduction,character.stories)
.addList(R.string.occurrences_events_list_introduction,character.events)
privatefunString.addList(introductionTextId:Int,list:List<String>):String{//3
if(list.isEmpty())returnthis
valintroductionText=getString(introductionTextId)
vallistText=list.joinToString(transform=
{"$bullet$it"},separator="\n")
returnthis+"$introductionText\n$listText\n\n"
}
companionobject{
privateconstvalbullet='\u2022'//4
privateconstvalCHARACTER_ARG="com.naxtlevelofandroiddevelopment.marvelgallery.presentation.heroprofile.CharacterArgKey"
funstart(context:Context,character:MarvelCharacter){
valintent=context
.getIntent<CharacterProfileActivity>()
.apply{putExtra(CHARACTER_ARG,character)}
context.startActivity(intent)
}
}
}
1. Thecompositionofthelistofoccurrencesisquiteacomplextask,soweextractittothefunctionmakeOccurrencesText.There,foreachoccurrencetype(comic,series,andsoon),wewanttoshowintroductiontextandlistonlyiftherearesomeoccurrencesofthistype.Wealsowanttoprefixeachitemwithabullet.
2. makeOccurrencesTextisasingleexpressionfunctionthatisusingaddListtoappendaninitiallyemptystringwiththenextliststhatwewanttodisplay.
3. addListisamemberextensionfunction.Itisreturningastringunchangediftheprovidedlistisempty,oritisreturningastringappendedwithintroductiontextandlistofelementswithbullets.
4. Thisisthecharacterthatisusedasalistbullet.
Wealsoneedtodefinestringsinstrings.xml:
<resources>
<stringname="app_name">MarvelGallery</string>
<stringname="marvel_copyright_notice">
DataprovidedbyMarvel.©2017MARVEL</string>
<stringname="search_hint">Searchforcharacter</string>
<stringname="occurrences_comics_list_introduction">Comics:</string>
<stringname="occurrences_series_list_introduction">Series:</string>
<stringname="occurrences_stories_list_introduction">Stories:</string>
<stringname="occurrences_events_list_introduction">Events:</string>
</resources>
Nowwecanseethewholecharacterprofile--charactername,image,description,andlistsofitsoccurrencesincomics,series,events,andstories:
SummaryTheapplicationiscomplete,buttherearestilllotsoffunctionalitiesthatcanbeadded.Inthisapplication,we'veseensomeexamplesofhowKotlincanbeusedtosimplifyAndroiddevelopment.Buttherearestillalotofsolutionstodiscover.KotlinsimplifiesAndroiddevelopmentatanylevel--fromcommonoperationssuchaslistenersetorviewelementreference,tohigh-levelfunctionalitiessuchasfunctionalprogrammingorcollectionprocessing.
ThisbookcannotsayeverythingaboutAndroiddevelopmentwithKotlin.Itwasdesignedtoshowenoughtoalloweveryonetostarttheirownadventurewithbaggagefullofideasandfeatureunderstanding.ThenextstepistoopenAndroidStudio,createyourownproject,andstarthavingfunwithKotlin.Thebigadventureisinfrontofyou.