androidandroid-fragmentsandroid-databinding

Android View Binding for a "base fragment"?


Android/Kotlin rookie here. I'm converting someone else's code, that was written with synthetics, to View Bindings per Google Play's upcoming deprecation deadline. I read this and this and converted the obvious stuff, but:

The developer set up 2 fragments that use a "base fragment", like this:

class FragmentA: BaseFragment() { ... }
class FragmentB: BaseFragment() { ... }
and then
open class BaseFragment: Fragment() { ... }

The base fragment references views in layouts associated with fragments A/B, using synthetics. (She could do that because she broke the layouts of fragments A/B into reusable components, and those two fragments are mutually exclusive -- only one of them may run at any given moment. Each of those reusable layout components has a binding class, I can see them, but I cannot reference them inside the base fragment, they're not inflated.)

My question: How do I get rid of the synthetic references in the base fragment? (Worst case, I copy paste everything and get rid of the base fragment, but wonder if there's a way to retain the design.)

Many thanks

7/21/23 adding some sample code per req. from @Obscure Cookie.

All those (ostensibly undefined) variables in the code come from the layouts imported at the top using android.synthetic. BaseGamesFragment doesn't have a layout per-se. The fragments that are based on it inflate the layouts.

import androidx.navigation.fragment.findNavController
import kotlinx.android.synthetic.main.fragment_fib.*
import kotlinx.android.synthetic.main.games_top_panel.*
import kotlinx.android.synthetic.main.hints_panel_games.*
import kotlinx.android.synthetic.main.no_headset_container.*
import kotlinx.android.synthetic.main.song_panel.*
import kotlinx.coroutines.launch

open class BaseGamesFragment : PitchBaseFragment() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        startForResult =
            registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {}
    }

    open fun initUI() {

        extra_button.setOnClickListener {
            try {
                // Exit games, then nav to settings
                val intent = Intent(activity, SettingsActivity::class.java)
                startActivity(intent)
                lifecycleScope.launch {
                    PSM.handleEvent(Event.SettingsButton93)
                }
            } catch (e: IllegalArgumentException) {
                Timber.e("LC: BaseGamesActivity initUI : %s", e.localizedMessage)
            }
        }

        btn_continue_anyway.setOnClickListener {
            displayDialog(no_headset_container, false)
            lifecycleScope.launch {
                // This is supposed to set the headsetSession to false
                PSM.handleEvent(Event.GenericFalse)
            }
        }
    }

    override fun updateSongPanel() {  // should be called song clock; just how deep we are into the song.
        song_panel?.let {
            SongLearningProgress.song?.let { currentSong ->
                currentLine?.let { currentLine ->
                    val (_, lyricsLine) = currentSong.findSectionAndLine(currentLine)
                    val startsAt = lyricsLine.startsAt.toInt()
                    song_progress_bar.progress = startsAt
                }
            }
        }
    }

    private fun showBottomSheet() {
        main_debug_bottom_sheet_contents.show()
        main_debug_bottom_sheet_contents.visibility = View.VISIBLE
    }

    open fun displayDialog(dialogType: String, visible: Boolean) {
        param = dialogType
        if (dialogType == "DNH1") {
            // display/hide no headset dialog
            if (visible) {
                view_flipper?.startFlipping()
            } else {
                view_flipper?.stopFlipping()
            }
            displayDialog(no_headset_container, visible)
        }
    }
}

Solution

  • I think the best practice to create a BaseFragment as below so that you have a reference to the ViewBinding from BaseFragment.

    abstract class BaseFragment<VBinding : ViewBinding, ViewModel : BaseViewModel> : Fragment() {
    
    open var useSharedViewModel: Boolean = false
    
    protected lateinit var viewModel: ViewModel
    protected abstract fun getViewModelClass(): Class<ViewModel>
    
    protected lateinit var binding: VBinding
    protected abstract fun getViewBinding(): VBinding
    
    private val disposableContainer = CompositeDisposable()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        init()
    }
    
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return binding.root
    }
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        setUpViews()
        observeData()
    }
    
    open fun setUpViews() {}
    
    open fun observeView() {}
    
    open fun observeData() {}
    
    private fun init() {
        binding = getViewBinding()
        viewModel = if (useSharedViewModel) {
            ViewModelProvider(requireActivity()).get(
                getViewModelClass()
            )
        } else {
            ViewModelProvider(this).get(getViewModelClass())
        }
    }
    
    fun Disposable.addToContainer() = disposableContainer.add(this)
    
    override fun onDestroyView() {
        disposableContainer.clear()
        super.onDestroyView()
    }}
    

    link