Complete App From Scratch: Kotlin Part 5 — Recyclerview, Adapter and ViewModel Setup
Welcome to Part 4 of the Remember app Series!
Welcome to Part 4 of the Remember app Series!

In the previous series of articles, we’ve seen how to configure the Room database for our app, how to set up the navigation component for navigating between different UI fragments within our app and finally, we also designed the repository class using Kotlin coroutines for managing the insertion, edit and getting a list of words back from the database — all done asynchronously.
If you’re new to this app series, here is the first article in the series. Please consider reading from the beginning to have a better understanding of what we’re trying to build and the different components and features we’re including in the app as well.
In this part, we’ll go about making the viewmodel class for our ListFragment and then, finally — connect the two components together to make sure it all works as expected! But first, let us make sure our word insert feature is working so that we can later get back the inserted list of words in our Recyclerview!
For your convenience, I’ve already uploaded the complete, working code for Remember in my GitHub, so please go ahead and check it out to see the complete app in action for yourself.
With these series of articles and the complete code at your disposal, understanding all the concepts involved in building this app will be a walk in the park! :)
Here’s the link to the repo:
yashprakash13/Remember-App
Store new words that you learn along with their pronunciation! Build your vocab! You can: Add new words that you learn…github.com
Now let’s get into the code!
Setting up the Insert feature
For our NewWordViewModel, we need to define a few functions:
First, we want the save word function, like so:
fun saveWord(word: Word){ repository.saveWordToDb(word)}
Next, we need to have ways to start and stop audio recording via the MediaRecorder library. startRecording function manages our timer for us:
fun startRecording(fileName: String) { _filename = fileName //start recording mediaRecorder = MediaRecorder().apply { setAudioSource(MediaRecorder.AudioSource.MIC) setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP) setOutputFile(fileName) setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB) try { prepare() } catch (e: IOException) { Log.e("AUDIO STATUS ERROR: ", "prepare() failed") } start() } //start the timer for 5 seconds timer = object : CountDownTimer(5000, 1000){ override fun onFinish() { //observer to know that recording needs to be stopped as time has exceeded //5 seconds _isTimeExceeded.value = true } override fun onTick(p0: Long) { //nothing to do here } } timer.start() //helps change text in the view through observer _isRecording.value = true _isRecorded.value = false //helps change the mic icon to play icon if (_isRecorded.value!!){ _isRecorded.value = false }}
And the stopRecording function too:
fun stopRecording() { //stop recording mediaRecorder?.apply { stop() release() } mediaRecorder = null //observer text and icon changes through these _isRecording.value = false _isRecorded.value = true //reset the timer boolean _isTimeExceeded.value = false //stop the timer timer.cancel()}
A few observable member variables of value can be noted in these two functions. Let’s define what they do.
Their declarations look like these, they are in fact MutableLiveData objects:
private val _isRecording = MutableLiveData<Boolean>()val isRecording : LiveData<Boolean> get() = _isRecording//if user has released the mic button and audio is recordedprivate val _isRecorded = MutableLiveData<Boolean>()val isRecorded : LiveData<Boolean> get() = _isRecorded//if audio is being played or notprivate val _isPlaying = MutableLiveData<Boolean>()val isPlaying : LiveData<Boolean> get() = _isPlaying
isRecording makes sure that when it’s set to true, a recording message is displayed to the user
isRecorded makes sure the user knows that a pronunciation has already been recorded and they can hear it/or record a new one
isPlaying is used to observe when the pronunciation is being played back through the MediaRecorder. An appropriate message is also displayed to the user.
In the NewWordFragment, we set the observable objects. We first implement the tap and hold to record audio:
//Implementing tap and hold to record for audiobtn_new_add_audio.setOnTouchListener(this)
Then, we change button states as/when required:
//when add audio btn is clicked, show the tap to record button and hide this buttonbtn_new_audio.setOnClickListener { //if audio has been recorded, play it back if (viewModel.isRecorded.value!!){ viewModel.playIfRecorded() }else { //otherwise, show the add audio button changeUIToAddAudio() }}
Next, we need to observe when the record is in progress.
//observe if recordingviewModel.isRecording.observe(viewLifecycleOwner, Observer { if (it){ showRecordingMessage() }})
When the audio has been recorded, we need to change the button states and display the message to the user:
//know if audio was recordedviewModel.isRecorded.observe(viewLifecycleOwner, Observer { if (it) { changeAudioButtonAppearances(true) }})
Finally, we also need to monitor when the recorded audio is being played:
//observe if playing the recorded audioviewModel.isPlaying.observe(viewLifecycleOwner, Observer { if (it){ showPlayingMessage() }})
And that is pretty much it for our insertion operation into the database!
Implementing our Recyclerview to display the list of words
We first define a coroutine scope for our viewmodel to run jobs.
//scope to perform db functions in viewmodelprivate var viewModelJob = Job()private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
Remember that we want to get the list of words in two ways? One in the alphabetical order and the other as the recently entered order? These two options will go in the options (a Bottomsheet) menu of our ListFragment later.
Let us define the two types of ordering of list of words in the init block of the WordsListViewModel:
init { /** * 0 means recent words, 1 means by alphabetical order */ wordsLiveData = if (sortBy == 0) { val factory: DataSource.Factory<Int, Word> = repository.getAllWordsRoomPaged() val pagedListBuilder = LivePagedListBuilder<Int, Word>(factory, 10) pagedListBuilder.build() }else{ val factoryAlpha: DataSource.Factory<Int, Word> = repository.getAllWordsRoomPagedAlphabetically() val pagedListBuilderAlpha = LivePagedListBuilder<Int, Word>(factoryAlpha, 10) pagedListBuilderAlpha.build() }}
Great! Now we can simply define the list of words to get as a livedata object!
fun getWordsPagedList() = wordsLiveData
where, wordsLiveData is declared as:
//pagedlist of all words to display in listprivate var wordsLiveData : LiveData<PagedList<Word>>
Okay then! All of that seems fine and now we just need to connect the components together by getting the list of words in our fragment class. But first, we need a WordsListAdapter as well.
The Adapter Setup
Our recyclerview adapter will need two different viewtypes for linear and staggered grid layout. Let’s define them via an enum class.
enum class ViewType { LINEAR, GRID}
Next, in the onCreateViewHolder, defining the two separate viewtypes is simple:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when(viewType){ ViewType.LINEAR.ordinal -> { val view = LayoutInflater.from(context).inflate(R.layout.each_word_item_view, parent, false) WordItemViewHolder(view) } else ->{ val view = LayoutInflater.from(context).inflate(R.layout.each_word_item_view_grid, parent, false) WordItemViewHolderGrid(view) } }}
Note that they use different layouts for displaying the words in the two viewtypes.
After that, setting up which viewtype to actually use, we do this:
override fun getItemViewType(position: Int): Int { return if (layoutManager.spanCount == 1) ViewType.LINEAR.ordinal else ViewType.GRID.ordinal}
We define two separate ViewHolders for the viewtypes, they are: WordItemViewHolder for list view and WordItemViewHolderGrid for grid view.
The rest of the adapter setup the quite similar to what you’ll normally do for any other app. Now we should go back to our ListFragment to connect our adapter and our viewmodel!
The Two Functions for the ListFragment
You read it right! We want two different types of words list sorted order in our app, hence we modularise it a little bit and write two functions for them, callable according to the menu we’ll later define in the bottomsheet through which the user will be able to change the layout any time they want.
Concretely, we want one function to give us a recent words paged list and the other to give us the alphabetically sorted order pagedlist.
Here is the helper function to get the list of words in recently inserted order:
/** * helper function to * set up observer on pagedlist * sort by recent words */private fun setUpRecentWordsPagedList() { val factory = WordsListViewModelFactory(WordRepository((WordDatabase.getInstance(requireActivity()))), 0) viewModel = ViewModelProvider(this, factory).get(WordsListViewModel::class.java) viewModel.getWordsPagedList().observe(viewLifecycleOwner, Observer {pagedList -> if (pagedList != null && pagedList.size > 0){ adapter.submitList(pagedList) recy_empty_view.dontShow() recy_words_list.show() }else{ Log.e("EMPTY:","recy") recy_words_list.dontShow() recy_empty_view.show() } })}
The alphabetical setup is pretty much identical to the above function.
Guess what the difference is? The sortBy argument we pass to the viewmodel will be 1 in this case, instead of the 0 in the previous case. Simple enough right?
This is the line that makes all the difference:
val factory = WordsListViewModelFactory(WordRepository((WordDatabase.getInstance(requireActivity()))), 1)
Here’s the complete function code:
/** * helper function to * set up observer on pagedlist * sort by alphabetically */private fun setUpAlphaWordsPagedList() { val factory = WordsListViewModelFactory(WordRepository((WordDatabase.getInstance(requireActivity()))), 1) viewModel = ViewModelProvider(this, factory).get(WordsListViewModel::class.java) viewModel.getWordsPagedList().observe(viewLifecycleOwner, Observer {pagedList -> if (pagedList != null && pagedList.size > 0){ adapter.submitList(pagedList) recy_empty_view.dontShow() recy_words_list.show() }else{ Log.e("EMPTY:","recy") recy_words_list.dontShow() recy_empty_view.show() } })}
Pheew! That seems pretty much done for our recyclerview setup and this article too!
Once again, I’ve uploaded the code for the app in my GitHub repo which you can view and run side by side while reading these articles to gain a better understanding of the workflow! Here is the repo.
Thank you for reading and I hope this was informative! Try to run the app and practise by exploring the code in detail. Don’t worry if you aren’t able to grasp everything mentioned in these articles! Remember, getting better at it is just some practise away. :)