Political Preparedness
Part 3b:Election- Fragments,ViewModel and ViewModelFactory
ViewModel
This class is designed to store and manage UI-related data in a lifecycle conscious manner. This allows data to survive configuration changes e.g screen rotations
This class ensures we do not assigning excessive responsibility to UI controllers. Doing so can result in a single class that tries to handle all of an app's work by itself, instead of delegating work to other classes.
If you create the ViewModel instance using the ViewModel class, a new object is created every time the fragment is re-created. Instead, create the ViewModel instance using a ViewModelProvider
The viewmodel never references fragments, activities or views (UI controller)
ElectionsViewModel.kt
import android.app.Application
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.android.politicalpreparedness.database.ElectionDatabase
import com.example.android.politicalpreparedness.database.ElectionsLocalRepository
import com.example.android.politicalpreparedness.network.models.Election
import kotlinx.coroutines.launch
// Construct ViewModel and provide election datasource
class ElectionsViewModel(application: Application): ViewModel() {
//database
private val database = ElectionDatabase.getInstance(application)
//the repository
private val electionsLocalRepository =ElectionsLocalRepository(database)
// Create live data val for upcoming elections
val upcomingElections: LiveData<List<Election>>
get() = electionsLocalRepository.elections
// Create live data val for saved elections
val savedElections: LiveData<List<Election>>
get() = electionsLocalRepository.electionsFollowed
// Create val and functions to populate live data for upcoming elections from the API and saved elections from local database
//init block
init {
viewModelScope.launch {
electionsLocalRepository.electionsRefreshed()
}
}
// Create functions to navigate to saved or upcoming election voter info
private val _moveToSelectedDetailsElection = MutableLiveData<Election>()
val navigateToSelectedUpcomingElection: LiveData<Election>
get() = _moveToSelectedDetailsElection
fun electionDetails(election: Election) {
_moveToSelectedDetailsElection.value = election
}
fun completeElection() {
_moveToSelectedDetailsElection.value = null
}
}
Check out ElectionsViewModel.kt on github
ViewModelFactory
It instantiates ViewModel objects, with or without constructor parameters.
This class will be responsible for instantiating the ElectionViewModel object with provided election datasource.
ElectionsViewModelFactory.kt
import android.app.Application
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import java.lang.IllegalArgumentException
// Create Factory to generate ElectionViewModel with provided election datasource
class ElectionsViewModelFactory(private val application: Application): ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ElectionsViewModel::class.java)) {
return ElectionsViewModel(application) as T
}
throw IllegalArgumentException(" ViewModel class is unknown")
}
}
Check out ElectionsViewModelFactory.kt on github
Fragments
Modular sections of an activity. They are reusable parts of your app's UI. They have their own lifecycle.They are more lightweight than activities. Fragments are hosted by an activity or another fragment.
ElectionsFragment.kt
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import com.example.android.politicalpreparedness.R
import com.example.android.politicalpreparedness.databinding.FragmentElectionBinding
import com.example.android.politicalpreparedness.election.adapter.ElectionListAdapter
import com.example.android.politicalpreparedness.election.adapter.ElectionListener
class ElectionsFragment: Fragment() {
// Declare ViewModel
private lateinit var electionsViewModel: ElectionsViewModel
private lateinit var upcomingElectionsListAdapter: ElectionListAdapter
private lateinit var savedElectionsListAdapter: ElectionListAdapter
private lateinit var fragmentElectionBinding: FragmentElectionBinding
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Add binding values
fragmentElectionBinding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_election,
container,
false)
fragmentElectionBinding.lifecycleOwner = this
// Add ViewModel values and create ViewModel
val electionsViewModelFactory = ElectionsViewModelFactory(requireActivity().application)
electionsViewModel = ViewModelProvider(this, electionsViewModelFactory).get(ElectionsViewModel::class.java)
fragmentElectionBinding.viewModel = electionsViewModel
// Link elections to voter info
//here we Observe the navigateToDetailElection LiveData and Navigate when it is not null.
electionsViewModel.upcomingElections.observe(viewLifecycleOwner, Observer {
it?.let {
upcomingElectionsListAdapter.submitList(it)
}
})
electionsViewModel.savedElections.observe(viewLifecycleOwner, Observer {
it?.let {
savedElectionsListAdapter.submitList(it)
}
})
// Initiate recycler adapters
// Populate recycler adapters
upcomingElectionsListAdapter = ElectionListAdapter(ElectionListener {
findNavController().navigate(
ElectionsFragmentDirections.actionElectionsFragmentToVoterInfoFragment(it.id, it.division))
})
fragmentElectionBinding.upcomingElectionsRv.adapter = upcomingElectionsListAdapter
// Setup Recycler View for saved elections
savedElectionsListAdapter = ElectionListAdapter(ElectionListener {
findNavController().navigate(
ElectionsFragmentDirections.actionElectionsFragmentToVoterInfoFragment(it.id, it.division))
})
fragmentElectionBinding.savedElectionsRv.adapter = savedElectionsListAdapter
return fragmentElectionBinding.root
}
// Refresh adapters when fragment loads
}
Check out ElectionsFragment.kt on github
VoterInfoViewModel.kt
import androidx.lifecycle.*
import com.example.android.politicalpreparedness.database.ElectionDao
import com.example.android.politicalpreparedness.network.CivicsApi
import com.example.android.politicalpreparedness.network.models.Division
import com.example.android.politicalpreparedness.network.models.VoterInfoResponse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class VoterInfoViewModel(private val electionDao: ElectionDao,
private val electionID:Int,
private val electionDivision: Division
) : ViewModel() {
// Add live data to hold voter info
private val _allVoterInfo = MutableLiveData<VoterInfoResponse>()
val allVoterInfo: LiveData<VoterInfoResponse>
get() = _allVoterInfo
// Add var and methods to populate voter info
init {
getAllVoterInfo()
}
private fun getAllVoterInfo() {
viewModelScope.launch {
var theAddress = "country:${electionDivision.country}"
if (!electionDivision.state.isBlank() && !electionDivision.state.isEmpty()) {
theAddress += "/state:${electionDivision.state}"
} else {
theAddress += "/state:ca"
}
_allVoterInfo.value = CivicsApi.retrofitService.getVoterInfoResponse(
theAddress, electionID)
}
}
// Add var and methods to support loading URLs
private val _votingLocations = MutableLiveData<String?>()
val votingLocations: LiveData<String?>
get() = _votingLocations
fun onClickVotingLocations() {
_votingLocations.value = _allVoterInfo.value?.state?.get(0)?.electionAdministrationBody?.votingLocationFinderUrl
}
fun navigateToVotingLocations() {
_votingLocations.value = null
}
private val _ballotInfo = MutableLiveData<String?>()
val ballotInfo: LiveData<String?>
get() = _ballotInfo
fun onClickBallotInfo() {
_votingLocations.value = _allVoterInfo.value?.state?.get(0)?.electionAdministrationBody?.ballotInfoUrl
}
fun navigateToBallotInformation() {
_ballotInfo.value = null
}
// Add var and methods to save and remove elections to local database
// cont'd -- Populate initial state of save button to reflect proper action based on election saved status
/**
* Hint: The saved state can be accomplished in multiple ways. It is directly related to how elections are saved/removed from the database.
*/
private val _isElectionSaved: LiveData<Int>
get() = electionDao.isElectionSaved(electionID)
val isFollowedElection =
Transformations.map(_isElectionSaved) { followValue ->
followValue?.let {
followValue == 1
}
}
fun unfollowAndFollowButton() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
if (isFollowedElection.value == true) {
electionDao.electionUnFollow(electionID)
} else {
electionDao.electionFollowed(electionID)
}
}
}
}
}
Check out VoterInfoViewModel.kt on github
VoterInfoViewModelFactory.kt
// Create Factory to generate VoterInfoViewModel with provided election datasource
class VoterInfoViewModelFactory(
private val electionDao: ElectionDao,
private val electionID: Int,
private val electionDivision: Division
): ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(VoterInfoViewModel::class.java)) {
return VoterInfoViewModel(electionDao, electionID, electionDivision) as T
}
throw IllegalArgumentException("Unknown VoterInfoViewModel")
}
}
Check out VoterInfoViewModelFactory.kt on github
VoterInfoFragment.kt
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.*
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.example.android.politicalpreparedness.R
import com.example.android.politicalpreparedness.database.ElectionDatabase
import com.example.android.politicalpreparedness.databinding.FragmentVoterInfoBinding
class VoterInfoFragment : Fragment() {
private lateinit var voterInfoViewModel: VoterInfoViewModel
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Add ViewModel values and create ViewModel
val bundle = VoterInfoFragmentArgs.fromBundle(requireArguments())
val electionID = bundle.argElectionId
val electionDivision = bundle.argDivision
val application = requireNotNull(this.activity).application
val electionDatabase = ElectionDatabase.getInstance(application).electionDao
val viewModelFactory = VoterInfoViewModelFactory(electionDatabase, electionID, electionDivision)
voterInfoViewModel = ViewModelProvider(this, viewModelFactory).get(VoterInfoViewModel::class.java)
// Add binding values
val fragmentVoterInfoBinding: FragmentVoterInfoBinding =
DataBindingUtil.inflate(inflater, R.layout.fragment_voter_info, container, false)
fragmentVoterInfoBinding.lifecycleOwner = this
fragmentVoterInfoBinding.viewModel = voterInfoViewModel
// Populate voter info -- hide views without provided data.
/**
Hint: You will need to ensure proper data is provided from previous fragment.
*/
// Handle loading of URLs
voterInfoViewModel.votingLocations.observe(viewLifecycleOwner, Observer {
it?.let {
loadingURLIntent(it)
voterInfoViewModel.navigateToVotingLocations()
}
})
voterInfoViewModel.ballotInfo.observe(viewLifecycleOwner, Observer {
it?.let {
loadingURLIntent(it)
voterInfoViewModel.navigateToBallotInformation()
}
})
// Handle save button UI state
// cont'd Handle save button clicks
voterInfoViewModel.isFollowedElection.observe(viewLifecycleOwner, Observer { wasElectionFollowed ->
if (wasElectionFollowed == true) {
fragmentVoterInfoBinding.saveElectionsButton.text = getString(R.string.unfollow_btn)
} else {
fragmentVoterInfoBinding.saveElectionsButton.text = getString(R.string.follow_btn)
}
})
return fragmentVoterInfoBinding.root
}
// Create method to load URL intents
private fun loadingURLIntent(url: String) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(intent)
}
}
Check out VoterInfoFragment.kt on github
Thank you so much for reading see you in part 4.