Political Preparedness

Part 3b:Election- Fragments,ViewModel and ViewModelFactory

Political Preparedness

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)

6451748b74d3b82c.png Source

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.