A.RandomCards-Room
database
A.0WhatisaRoomdatabase?
Adatabaseisanorganizedcollectionofstructuredinformation,ordata,
typicallystoredelectronicallyinacomputersystem.Adatabaseisusually
controlledbyadatabasemanagementsystem(DBMS).[oracle.com]Android
usesSQLiteasadatabasemanagementsystem.
FormoredetailscheckthecontentprovidedunderWeek7>LectureMaterial>
DatabaseandSQL
BEFOREYOUBEGIN:Downloadthefollowingapplicationprojectasastarterapplication.
RandomCardsDatabase_Start.zip
AboveappisoutputofWeek6LabTask>C.GsontosaveArrayListtoSharedPreferences.Wewill
replaceSharedPreferencesstorelocationofthisapplicationprojectwithRoomDatabase.
A.1Importdependencies
RoomdatabaselibraryisavailablefromtheAndroidXpackage,toimportthemnavigate
toapp>GradleScripts>build.gradle.kts(Module:app)
Underthedependenciessectionaddtheselines:
代
码
:
//importRoomdatabasecommonandruntimepackages
implementation("androidx.room:room-common:2.6.1")
implementation("androidx.room:room-runtime:2.6.1")
//toparseannotationseg@Database,@Entity,@DAO,etc
annotationProcessor("androidx.room:room-compiler:2.6.1")
Onceaddedclickon"SyncNow"whichappearsatthetoponcechangesaredetectedinthefile.
A.2AddnewDatabaseClass
BeforeaddingthenewDatabaseclasslet'scontaineverythinginsideasubfoldertohaveseparationofconcern
andalldatabase-relatedfileswillbecontainedinsideonefolder.
Right-clickonthepackagefolder(com.fit2081.recyclerviewcards)andaddapackagecalled"provider".
Seemoredetailshere...
NowgoaheadandcreateanewJavaclassinsidethisnewpackage,named
"CardDatabase"andcarefullyreplacetheclassimplementationwiththeprovidedcode.
packagecom.fit2081.recyclerviewcards.provider;
importandroid.content.Context;
importandroidx.room.Database;
importandroidx.room.Room;
importandroidx.room.RoomDatabase;
importjava.util.concurrent.ExecutorService;
importjava.util.concurrent.Executors;
@Database(entities={},version=1)
publicabstractclassCardDatabaseextendsRoomDatabase{
//databasename,thisisimportantasdataiscontainedinsideafilenamed
"card_database"
publicstaticfinalStringCARD_DATABASE="card_database";
//markingtheinstanceasvolatiletoensureatomicaccesstothevariable
privatestaticvolatileCardDatabaseINSTANCE;
privatestaticfinalintNUMBER_OF_THREADS=4;
//ExecutorServiceisaJDKAPIthatsimplifiesrunningtasksinasynchronous
mode.
//Generallyspeaking,ExecutorServiceautomaticallyprovidesapoolofthreads
andanAPI
//forassigningtaskstoit.
staticfinalExecutorServicedatabaseWriteExecutor=
Executors.newFixedThreadPool(NUMBER_OF_THREADS);
}
Thisway,wecommunicatewithruntimeandprocessortonotreorderanyinstructions
involvingthevolatilevariable.Also,processorsunderstandthattheyshouldimmediately
makeanyupdatestothesevariables.
Nowbecausethisisanabstractclass,weneedaddadatabasereferenceforan
implementation.
staticfinalExecutorServicedatabaseWriteExecutor=
Executors.newFixedThreadPool(NUMBER_OF_THREADS);
/**
*Sincethisclassisanabsractclass,togetthedatabasereferencewewould
need
*toimplementawaytogetreferencetothedatabase.
*
*@paramcontextApplicationofActivityContext
*@returnareferencetothedatabaseforreadandwriteoperation
*/
staticCardDatabasegetDatabase(finalContextcontext){
if(INSTANCE==null){
synchronized(CardDatabase.class){
if(INSTANCE==null){
INSTANCE=
Room.databaseBuilder(context.getApplicationContext(),
CardDatabase.class,CARD_DATABASE)
.build();
}
}
}
returnINSTANCE;
}
Pleasenotethesynchronizedkeywordabove.
Synchronizedblocksormethodspreventsthreadinterferenceandmakesure
thatdataisconsistent.Atanypointoftime,onlyonethreadcanaccessa
synchronizedblockormethod(criticalsection)byacquiringalock.Other
thread(s)willwaitforreleaseoflocktoaccesscriticalsection.Methodsare
synchronizedwhenyouadd
synchronized
tomethoddefinitionordeclaration.
Youcanalsosynchronizeaparticularblockofcodewith-inamethod.
Itmeansthatonlyonethreadcanaccesscriticalsectionbyacquiringalock.
Unlessthisthreadreleasethislock,allotherthread(s)willhavetowaitto
acquirealock.Theydon'thaveaccesstoentercriticalsectionwithout
acquiringlock.It'sprogrammerresponsibilitytoidentifycriticalsection(s)in
applicationandguarditaccordingly.Javaprovidesaframeworktoguardyour
application,butwhichsectionstobeguardedistheresponsibilityof
programmer.Source
A.3Transformthe"Item"classtoRoomEntity
OpenItem.javaandimplementthefollowing
A.3.1Entityannotation
Inthelinebefore"publicclassItem"add"@Entity"annotationthiswillidentifythisclass
asaRoomDatabaseentity.Anentityisequivalenttoadatabasetable.Youcangiveita
tableNamehereaswell(sowecanaccessitlaterusingthisname).
@Entity(tableName="items")
A.3.2Addnewcolumn"Id"
Toidentifyeachrecordinthetable,weaddthis"id"field,wewantthisfield'svaluetobe
auto-generatedonthesaveofanewrecord,thiswaywedonothavetoworryabout
implementingourcustomuniquenesslogic.
@PrimaryKey(autoGenerate=true)
@ColumnInfo(name="id")
privateintid;
Ifyouopttocopyandpastetheabovecode,don'tforgettoresolvedependencies.
A.3.3Annotateotherclassattributesusingthe"ColumnInfo"annotation
ColumnInfoannotationhelpsusmarkclassattributesasdatabasecolumnsandspecify
theattribute'scolumnnameinthesameline.
@ColumnInfo(name="suit")
privateStringsuit;
@ColumnInfo(name="card")
privateStringcard;
A.3.4AddGetter&Settermethodfor"Id"column
Right-clickwithinItem.javaclass>Generate>GetterandSetter>Selectonlythe"id"
columnasothercolumnsalreadyhavetheirgettermethod&setisdoneusingthe
constructorclass.
HereisthecompletedcodeoftheItem.javaclass,noticethatlinenumber7specifies
thetablename"items"
packagecom.fit2081.recyclerviewcards;
importandroidx.room.ColumnInfo;
importandroidx.room.Entity;
importandroidx.room.PrimaryKey;
@Entity(tableName="items")
publicclassItem{
@PrimaryKey(autoGenerate=true)
@ColumnInfo(name="id")
privateintid;
@ColumnInfo(name="columnSuit")
privateStringsuit;
@ColumnInfo(name="columnCard")
privateStringcard;
publicItem(Stringsuit,Stringcard){
this.suit=suit;
this.card=card;
}
publicStringgetSuit(){
returnsuit;
}
publicStringgetCard(){
returncard;
}
publicintgetId(){
returnid;
}
publicvoidsetId(intid){
this.id=id;
}
}
Important:NowgobacktoCardDatabaseclassanddeclarethisentityclassasoneofthedatabase
tablesofCardDatabase.java,asshownbelow.YoucandothisabovethedeclarationofCardDatabse-
seebelow.
JAVA
@Database(entities={Item.class},version=1)
publicabstractclassCardDatabaseextendsRoomDatabase{
A.4Adddataaccessobject-DAO
Addanewinterfacenamed"CardDAO",makesuretoselect"interface"beforeyouhit
enterafterspecifyingthename,seethestepsbelow.
1.Addannotation"@Dao"tothisinterface,whichstandsforDataAccessObject
2.Addtwokeymethods:
○Insertnewitemrecord(C-fromCRUD)
○Getallitemsfromthedatabase(R-fromCRUD)
HereisthecompletedcodeforDAO,whichusesaLiveDataholder(seelectureslides)
LiveDataisanobservabledataholderclass.Unlikearegularobservable,
LiveDataislifecycle-aware,meaningitrespectsthelifecycleofotherapp
components,suchasactivities,fragments,orservices.Thisawareness
ensuresLiveDataonlyupdatesappcomponentobserversthatareinanactive
lifecyclestate.Source
JAVA
packagecom.fit2081.recyclerviewcards.provider;
importandroidx.lifecycle.LiveData;
importandroidx.room.Dao;
importandroidx.room.Insert;
importandroidx.room.Query;
importcom.fit2081.recyclerviewcards.Item;
importjava.util.List;
//IndicatesthatthisinterfaceisaDataAccessObject(DAO),
//usedforinteractingwiththedatabase.
@Dao
publicinterfaceCardDAO{
////Specifiesadatabasequerytoretrieveallitemsfromthe"items"table.(referenced
A.3.4)
@Query("select*fromitems")
LiveData>getAllItems();//ReturnsaLiveDataobjectcontainingalistof
Itemobjects.
//Indicatesthatthismethodisusedtoinsertdataintothedatabase.
@Insert
voidaddItem(Itemitem);//MethodsignatureforinsertinganItemobjectintothe
database.
}
NoticetableNameattributewespecifiedinA.3.4isreferencedinline15above.
InitialisetheDAOvariableinsidetheCardDatabaseclassasshownbelow.
//referencetoDAO,hereRoomDatabaseparentclasswillimplementthisinterface
publicabstractCardDAOcardDAO();
A.5CreateRepositoryclass
CreateanewJavaclass"CardRepository"andreplacethecodespecifiedbelow,read
inlinecommentsformoredetails.
InAndroiddevelopmentwithRoom,arepositoryclasstypicallyinteractswith
theRoomdatabasethroughDAOs(DataAccessObjects)andprovides
methodsforaccessingandmanipulatingdata.Thishelpstokeepthedata
accesslogicseparatefromtheUIlogic,followingtheprinciplesofseparationof
concernsandsingleresponsibility.Thinkaboutit.Whenyouhavemultipledata
sources,inthatcase,therepositorywillactasasinglesourceoftruthhaving
dataavailablefromdifferentsourcesandservingthedatatotheViewModel
class.
packagecom.fit2081.recyclerviewcards.provider;
importandroid.app.Application;
importandroidx.lifecycle.LiveData;
importcom.fit2081.recyclerviewcards.Item;
importjava.util.List;
publicclassCardRepository{
//privateclassvariabletoholdreferencetoDAO
privateCardDAOcardDAO;
//privateclassvariabletotemporaryholdalltheitemsretrievedandpassoutsideof
thisclass
privateLiveData>allCardsLiveData;
//constructortoinitialisetherepositoryclass
CardRepository(Applicationapplication){
//getreference/instanceofthedatabase
CardDatabasedb=CardDatabase.getDatabase(application);
//getreferencetoDAO,toperformCRUDoperations
cardDAO=db.cardDAO();
//oncetheclassisinitialisedgetalltheitemsintheformofLiveData
allCardsLiveData=cardDAO.getAllItems();
}
/**
*Repositorymethodtogetallcards
*@returnLiveDataoftypeList
*/
LiveData>getAllCards(){
returnallCardsLiveData;
}
/**
*Repositorymethodtoinsertonesingleitem
*@paramcardobjectcontainingdetailsofnewItemtobeinserted
*/
voidinsert(Itemcard){
//Executesthedatabaseoperationtoinserttheiteminabackgroundthread.
CardDatabase.databaseWriteExecutor.execute(()->cardDAO.addItem(card));
}
}
Letstakeacloserlookatthiscode..
CardDatabase.databaseWriteExecutor.execute(()->cardDAO.addItem(card))
:
Thislineofcodeexecutesadatabaseoperationtoinserttheitem.It'susinganexecutor
(
databaseWriteExecutor
)torunthisoperationonabackgroundthreadtoavoid
blockingthemainUIthread.The
execute()
methodtakesalambdaexpression(
()->
cardDAO.addItem(card)
)asanargument,whichencapsulatesthedatabaseoperation
tobeexecuted.
A.6CreateViewModelclass
ViewModelclassisusedforpre-processingthedata,beforepassingittothecontrollers
(ActivityorFragments).ViewModelclassshouldnotholddirectreferencetothe
database.ViewModelclassreliesontherepositoryclass,hencethedatabaseis
accessedusingtheRepositoryclass.
CreateanewViewModelclasscallit"CardViewModel"andusetheprovidedcodeto
completeit,readinlinecommentsformoredetails.
packagecom.fit2081.recyclerviewcards.provider;
importandroid.app.Application;
importandroidx.annotation.NonNull;
importandroidx.lifecycle.AndroidViewModel;
importandroidx.lifecycle.LiveData;
importcom.fit2081.recyclerviewcards.Item;
importjava.util.List;
/**
*ViewModelclassisusedforpre-processingthedata,
*beforepassingittothecontrollers(ActivityorFragments).ViewModelclassshouldnot
hold
*directreferencetodatabase.ViewModelclassreliesonrepositoryclass,hencethe
databaseis
*accessedusingtheRepositoryclass.
*/
publicclassCardViewModelextendsAndroidViewModel{
//referencetoCardRepository
privateCardRepositoryrepository;
//privateclassvariabletotemporaryholdalltheitemsretrievedandpassoutsideof
thisclass
privateLiveData>allCardsLiveData;
publicCardViewModel(@NonNullApplicationapplication){
super(application);
//getreferencetotherepositoryclass
repository=newCardRepository(application);
//getallitemsbycallingmethoddefinedinrepositoryclass
allCardsLiveData=repository.getAllCards();
}
/**
*ViewModelmethodtogetallcards
*@returnLiveDataoftypeList
*/
publicLiveData>getAllCards(){
returnallCardsLiveData;
}
/**
*ViewModelmethodtoinsertonesingleitem,
*usuallycallinginsertmethoddefinedinrepositoryclass
*@paramcardobjectcontainingdetailsofnewItemtobeinserted
*/
publicvoidinsert(Itemcard){
repository.insert(card);
}
}
NowbothViewModel&Repositoryclassesaredefined,andanytimeMainActivityneeds
dataorneedstoinsertdataitwillgointhissequence.
MainActivity>ViewModel>Repository>DAO>Database.
Thisisabreakdownofourfiles(approximation):
A.7Insertarandomcard
GotoMainActivityanddeclareaclassvariabletoholdareferencetoCardViewModel.
privateCardViewModelcardViewModel;
insideonCreatemethodofMainActivityinitialiseViewModel.
//initialiseViewModel
cardViewModel=newViewModelProvider(this).get(CardViewModel.class);
UpdatetheaddItemmethodasperbelowbycallingCardViewModel'sinsertmethod.
publicvoidaddItem(){
Randomrandom=newRandom();
intrandCard=random.nextInt(cards.length);
intrandSuit=random.nextInt(suits.length);
Itemitem=newItem(suits[randSuit],cards[randCard]);
//insertnewrecordtodatabase
cardViewModel.insert(item);
//noneedofnotifyDataSetChangedhereasitwillbedonebyLiveDatasubscription
above
//data.add(item);
//adapter.notifyDataSetChanged();
//savethewholelisttoSharedPreferencesoneachaddItem
//saveArrayListAsText();
}
A.8Readallcards&displayusingRecyclerView
InsidetheonCreatemethodafterinitiatingtheCardViewModel,subscribetoLiveDataof
typeArrayList
//subscribetoLiveDataoftypeArrayList
//anychangesdetectedinthedatabasewillbenotifiedtoMainActivity
cardViewModel.getAllCards().observe(this,newData->{
//castList
adapter.setData(newArrayList
adapter.notifyDataSetChanged();
});
OpentheAppInspectionwindowtoinspectthedatabasewithinAndroidStudio,even
thoughitisstoredinthevirtualandroiddevice.
Tips
●Tickthe"Liveupdates"boxashighlightedbelowseeredhighlight.
●ThetablenamewedefinedinItem.javaline7,isvisibleasoneoftheoptions
underthelistoftables,seethegreenhighlight.
●Similarly,thecolumnnamesarevisibleinthesnippetbelow,seetheblue
highlights.
Welldone!Thiswasamassiveeffort,youhavecompleteddatabaseimplementationin
anAndroidproject!