I am creating an android application with Navigation Component implementation. The workflow of SplashFragment (which is startDestination) is that onViewCreated Admob Interstitial request is sent and AD displayed, on ad close i want to navigate the app to DashboardFragment. The navigation works fine when Admob AD is not displayed but after Admob AD displayed the navigation never works and UI is stuck on SplashFragment.
What i tried?
I tried to resolve this issue by navigating the fragment onAdShowedFullScreenContent instead of onAdDismissedFullScreenContent but this results in showing a black screen before Admob AD display.
Below is my code
SplashFragment
class SplashFragment : Fragment() {
private var onFinishedFirstCall: Boolean = false
//max wait time is 10 seconds
val SPLASH_TIME_SHORT_WITHOUT_AD = 2000
private var isAdMobIntLoaded = false
var handler = Handler()
private val repository = get<Repository>()
var interstitialadMobAd: InterstitialAd? = null
var isShow = false
private val sharedViewModel: MainViewModel by sharedViewModel()
private val tinyDB = get<TinyDB>()
var isRemoteConfigLoaded = false
var isInitialSetupDone = false
var timer: CountDownTimer? = null
var counter = 4
private var timerStart = false
var remaining: Long = 6000
var isAdReqCompleted = false
override fun onCreate(savedInstanceState: Bundle?) {
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.splash_screen_layout, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
nativeRemoteCheck()
}
private fun nativeRemoteCheck() {
repository.onRemoteConfigLoadedListener.observe(
viewLifecycleOwner,
object : Observer<Boolean> {
override fun onChanged(isLoaded: Boolean?) {
//reset saved ad request info which was saved to avoid ad recalls on connectivity changes
resetAdRequestInfo(tinyDB)
if (isLoaded!!) {
isRemoteConfigLoaded = true
initialScreenSetup()
}
}
})
}
private fun initialScreenSetup() {
//Remote config response receive; now proceed
//if purchased or no connectivity proceed with 2 seconds delay
val isInternetConnected = requireContext().isNetworkAvailable()
if (sharedViewModel.getIsPurchased() || !isInternetConnected) {
isInitialSetupDone = true
setupTimer(2000)
} else {
setupTimer(10000)
//not purchased and internet is also connected
sharedViewModel.getSplashIntersConfig()
.observe(viewLifecycleOwner, Observer {
if (it.show) {
isInitialSetupDone = true
//if splash interstial is enabled then send request
adMobExtensionFuntion()
} else {
isInitialSetupDone = true
//if splash interstial ad is not enabled proceed with 2 seconds delay
handler.postDelayed({
nextPage()
}, SPLASH_TIME_SHORT_WITHOUT_AD.toLong())
}
})
}
}
private fun nextPage() {
try {
val navHostFragment =
activity?.supportFragmentManager?.findFragmentById(R.id.main_navigation_graph) as NavHostFragment
val navController = navHostFragment.navController
navController.navigate(R.id.action_splash_to_dashboard)
} catch (ex: Exception) {
}
}
fun adMobExtensionFuntion() {
context?.loadAdmobInterstitial(
AdPlacement.SPLASH,
context?.getString(R.string.admob_Splash_int)!!,
{
interstitialadMobAd = it
sharedViewModel.onIntAdSuccess.postValue(true)
isAdMobIntLoaded = true
isAdReqCompleted = true
interstitialadMobAd?.fullScreenContentCallback =
object : FullScreenContentCallback() {
override fun onAdDismissedFullScreenContent() {
nextPage()
}
override fun onAdFailedToShowFullScreenContent(adError: AdError?) {
}
override fun onAdShowedFullScreenContent() {
}
}
},
{
isAdReqCompleted = true
nextPage()
}
)
}
fun setupTimer(duration: Long) {
timerStart = true
timer = object : CountDownTimer(duration, 1000) {
override fun onTick(millisUntilFinished: Long) {
Log.e("timer1 :", ": ${millisUntilFinished}")
remaining = millisUntilFinished
if (interstitialadMobAd != null) {
onFinish()
}
}
override fun onFinish() {
onFinishedFirstCall = true
if (interstitialadMobAd != null) {
interstitialadMobAd?.show(activity)
timer?.cancel()
} else {
if (interstitialadMobAd == null && !isAdReqCompleted) {
nextPage()
}
timer?.cancel()
}
}
}.start()
}
Navigation Graph
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/main_navigation_graph"
app:startDestination="#id/splash_fragment">
<fragment
android:id="#+id/splash_fragment"
android:name="mypkgname.ui.splash.SplashFragment"
tools:layout="#layout/splash_screen_layout">
<action
android:id="#+id/action_splash_to_dashboard"
app:destination="#id/dashboard_fragment"
/>
</fragment>
<fragment
android:id="#+id/dashboard_fragment"
android:name="mypkgname.ui.dashboard.DashboardFragment"
tools:layout="#layout/test_dashboard_layout">
<action
android:id="#+id/action_dashboard_to_videoplayer"
app:destination="#id/videoplayer_fragment"
app:enterAnim="#anim/enter_animation" />
</fragment>
<fragment
android:id="#+id/videoplayer_fragment"
android:name="mypkgname.ui.videoplayer.VideoPlayerFragment"
tools:layout="#layout/fragment_videoplayer_layout">
<action
android:id="#+id/action_videoplayer_to_dashboard"
app:destination="#id/dashboard_fragment"
app:exitAnim="#anim/nav_default_exit_anim"
/>
</fragment>
</navigation>
Please note i am using this sdk version of Admob
api 'com.google.android.gms:play-services-ads:20.4.0'
I am unable to figure out the cause of this issue. Can somebody please help me out with this.
Thank you
Related
I am passing TextToSpeech from Fragment to my RecyclerView Adapter.
While passing it, I am also sending a flag textToSpeechSupported to confirm whether the currently set device language is supported for TextToSpeech announcement or not.
But every time the value of this flag is being set as false, even though I am setting the value to true in onCreate.
It seems there is an issue with my implementation approach.
But I tired to debug and also added Logs.
Also I did multiple test and tried various other combinations. But every time the flag textToSpeechSupported value is being passed is false, even if the language is supported.
Am I missing something here.
I need the flag textToSpeechSupported value to be true if the device language is supported by TextToSpeech
Please guide.
class WelcomeFragment : Fragment() {
private lateinit var welcomeAdapter: WelcomeAdapter
private lateinit var textToSpeech: TextToSpeech
private var textToSpeechSupported: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
textToSpeech = TextToSpeech(requireContext()) { status ->
if (status == SUCCESS) {
val result = textToSpeech.setLanguage(Locale.getDefault())
textToSpeechSupported =
!(result == TextToSpeech.LANG_NOT_SUPPORTED || result == TextToSpeech.LANG_MISSING_DATA)
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
welcomeAdapter = WelcomeAdapter(textToSpeech, textToSpeechSupported)
binding.adapter = welcomeAdapter
}
}
class Welcomedapter(private val textToSpeech: TextToSpeech, private val textToSpeechSupported: Boolean) : ListAdapter<Welcome, ViewHolder>(WelcomeDiffCallback()) {
//....
class ViewHolder private constructor(val binding: ContainerWelcomeBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: Welcome, textToSpeech: TextToSpeech, textToSpeechSupported: Boolean) {
binding.apply {
welcomeMessageText.text = item.welcome
textToSpeechImage.setOnClickListener {
if (textToSpeechSupported) {
textToSpeech.speak(item.welcome, TextToSpeech.QUEUE_FLUSH, null)
} else {
// Send an event for Toast saying that language is not supported for Text to Speech
}
}
}
}
}
}
The objective/goal is to ensure that value of flag textToSpeechSupported is correctly calculated and passed to Recycler View Adapter.
The problem is that onViewCreated is being called BEFORE the tts has had time to initialize, so you are accessing textToSpeechSupported too early and you're always getting your default (false) value.
So, instead of calling:
welcomeAdapter = WelcomeAdapter(textToSpeech, textToSpeechSupported)
binding.adapter = welcomeAdapter
from inside onViewCreated, make a new function:
fun thisFunctionRunsAFTERtheTTSisInitialized() {
// put that code here instead.
}
And then call that function from inside your onCreate so it will run AFTER the tts has initialized:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
textToSpeech = TextToSpeech(requireContext()) { status ->
if (status == SUCCESS) {
val result = textToSpeech.setLanguage(Locale.getDefault())
textToSpeechSupported =
!(result == TextToSpeech.LANG_NOT_SUPPORTED || result == TextToSpeech.LANG_MISSING_DATA)
thisFunctionRunsAFTERtheTTSisInitialized() // <---------------
}
}
}
It would be better to pass an interface or function to adapter
e.g
class Welcomedapter(private val block : () -> Unit) : ListAdapter<Welcome, ViewHolder>(WelcomeDiffCallback()) {
//....
class ViewHolder private constructor(val binding: ContainerWelcomeBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: Welcome, textToSpeech: TextToSpeech, textToSpeechSupported: Boolean) {
binding.apply {
welcomeMessageText.text = item.welcome
textToSpeechImage.setOnClickListener {
block()
}
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
welcomeAdapter = WelcomeAdapter {
if (textToSpeechSupported) {
textToSpeech.speak(item.welcome, TextToSpeech.QUEUE_FLUSH, null)
} else {
// show the toast here now
}
}
binding.adapter = welcomeAdapter
}
I want to show progress bar on the screen untill all the required data is fetched from firebase database.
How can I use the below code in the fetchMenu() [in MenuFragment.kt]
.addOnSuccessListener(OnSuccessListener<Void?> { // Write was successful!
// ...
progressBar.visibility = View.GONE
})
.addOnFailureListener(OnFailureListener { // Write failed
// ...
progressBar.visibility = View.GONE
})
MenuFragment.kt
class MenuFragment : Fragment() {
private var dishList: MutableList<DishModel> = mutableListOf()
private lateinit var myRef: DatabaseReference
lateinit var list: RecyclerView
lateinit var proceedToCartLayout: RelativeLayout
lateinit var addToCartBtn: Button
private var selectedCategory = ""
lateinit var progressLayout: RelativeLayout
lateinit var progressBar: ProgressBar
companion object {
fun newInstance(): Fragment {
return MenuFragment()
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_menu, container, false)
//retrieve id
val bundle = this.arguments
selectedCategory = bundle!!.getString("CATEGORY_ID")!!
list = view.findViewById(R.id.recyclerMenu)
myRef = FirebaseDatabase.getInstance().getReference("Category")
proceedToCartLayout = view.findViewById(R.id.ProceedToCart)
addToCartBtn = view.findViewById(R.id.btn_cart)
progressLayout = view.findViewById(R.id.progressLayout)
progressBar = view.findViewById(R.id.progressBar)
return view
}
override fun onResume() {
if (ConnectionManager().checkConnectivity(activity as Context)) {
fetchMenu()
} else {
val alterDialog = androidx.appcompat.app.AlertDialog.Builder(activity as
Context)
alterDialog.setTitle("No Internet")
alterDialog.setMessage("Connect to internet to continue")
alterDialog.setIcon(R.drawable.nointernet)
alterDialog.setPositiveButton("Open Settings") { _, _ ->
val settingsIntent = Intent(Settings.ACTION_SETTINGS)//open wifi settings
startActivity(settingsIntent)
}
alterDialog.setNegativeButton("Exit") { _, _ ->
ActivityCompat.finishAffinity(activity as Activity)
}
alterDialog.setCancelable(false)
alterDialog.create()
alterDialog.show()
}
super.onResume()
}
private fun fetchMenu() {
progressLayout.visibility = View.VISIBLE
myRef.child(selectedCategory)
.addValueEventListener(object : ValueEventListener {
override fun onCancelled(p0: DatabaseError) {
progressLayout.visibility = View.GONE
Toast.makeText(context, "$p0", Toast.LENGTH_SHORT).show()
}
override fun onDataChange(p0: DataSnapshot) {
progressLayout.visibility = View.GONE
if (p0.exists()) {
dishList.clear()
for (i in p0.children) {
val plan = i.getValue(DishModel::class.java)
dishList.add(plan!!)
}
val adapter = MenuAdapter(
context!!,
R.layout.menu_list_item,
dishList,
proceedToCartLayout,
addToCartBtn, selectedCategory
)
list.adapter = adapter
}
}
})
}
}
I got the result after using progress bar but didn't get the required result as I want to display the progress bar untill all the rows of the RecyclerView get filled with the data fetched from the firebase database and all the views correctly placed.
I'm getting this while using progress bar
see the video
According to your shared image, you aren't displaying the "ProgressBar" at all. To solve this, please uncomment the following line of code:
progressLayout.visibility = View.VISIBLE
And add the following line:
progressLayout.visibility = View.GONE
Inside the "onDataChange()" method as well. In this way you are starting to display the ProgressBar when the "fetchMenu()" method is called and once you get a response, either the data or an Exception you can hide it.
I am using live data from a shared ViewModel across multiple fragments. I have a sign-in fragment which takes user's phone number and password and then the user presses sign in button I am calling the API for that, now if the sign-in fails I am showing a toast "Sign In failed", now if the user goes to "ForgotPassword" screen which also uses the same view model as "SignInFragment" and presses back from the forgot password screen, it comes to sign-in fragment, but it again shows the toast "Sign In failed" but the API is not called, it gets data from the previously registered observer, so is there any way to fix this?
SignInFragment.kt
class SignInFragment : Fragment() {
private lateinit var binding: FragmentSignInBinding
//Shared view model across two fragments
private val onBoardViewModel by activityViewModels<OnBoardViewModel>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_sign_in,
container,
false
)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onBoardViewModel.signInResponse.observe(viewLifecycleOwner) { response ->
//This is calling again after coming back from new fragment it.
showToast("Sign In Failed")
}
}
override fun onClick(v: View?) {
when (v?.id!!) {
R.id.forgotPasswordTV -> {
findNavController().navigate(SignInFragmentDirections.actionSignInFragmentToForgotPasswordFragment())
}
R.id.signInTV -> {
val phoneNumber = binding.phoneNumberET.text
val password = binding.passwordET.text
val signInRequestModel = SignInRequestModel(
phoneNumber.toString(),
password.toString(),
""
)
//Calling API for the sign-in
onBoardViewModel.callSignInAPI(signInRequestModel)
}
}
}
}
ForgotPasswordFragment
class ForgotPasswordFragment : Fragment() {
private lateinit var binding: FragmentForgotPasswordBinding
//Shared view model across two fragments
private val onBoardViewModel by activityViewModels<OnBoardViewModel>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_forgot_password,
container,
false
)
return binding.root
}
}
OnBoardViewModel
class OnBoardViewModel : ViewModel() {
private var repository: OnBoardRepository = OnBoardRepository.getInstance()
private val signInRequestLiveData = MutableLiveData<SignInRequestModel>()
//Observing this data in sign in fragment
val signInResponse: LiveData<APIResource<SignInResponse>> =
signInRequestLiveData.switchMap {
repository.callSignInAPI(it)
}
//Calling this function from sign in fragment
fun callSignInAPI(signInRequestModel: SignInRequestModel) {
signInRequestLiveData.value = signInRequestModel
}
override fun onCleared() {
super.onCleared()
repository.clearRepo()
}
}
I have tried to move this code inside onActivityCreated but it's still getting called after coming back from new fragment.
onBoardViewModel.signInResponse.observe(viewLifecycleOwner) { response ->
showToast("Sign In Failed")
}
Using SingleLiveEvent class instead of LiveData in OnBoardViewModel class will solve your problem:
val signInResponse: SingleLiveEvent <APIResource<SignInResponse>>.
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val pending = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, Observer<T> { t ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
override fun setValue(t: T?) {
pending.set(true)
super.setValue(t)
}
fun call() {
postValue(null)
}
}
This is a lifecycle-aware observable that sends only new updates after subscription. This LiveData only calls the observable if there's an explicit call to setValue() or call().
I would provide a way to reset your live data. Give it a nullable type. Your observers can ignore it when they get a null value. Call this function when you receive login data, so you also won't be repeating messages on a screen rotation.
class OnBoardViewModel : ViewModel() {
// ...
fun consumeSignInResponse() {
signInRequestLiveData.value = null
}
}
onBoardViewModel.signInResponse.observe(viewLifecycleOwner) { response ->
if (response != null) {
showToast("Sign In Failed")
onBoardViewModel.consumeSignInResponse()
}
}
For Kotlin users #Sergey answer can also be implemented using delegates like below
class SingleLiveEvent<T> : MutableLiveData<T>() {
var curUser: Boolean by Delegates.vetoable(false) { property, oldValue, newValue ->
newValue != oldValue
}
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, Observer<T> { t ->
if (curUser) {
observer.onChanged(t)
curUser = false
}
})
}
override fun setValue(t: T?) {
curUser = true
super.setValue(t)
}
fun call() {
postValue(null)
}
}
so, I'm using the Android Navigation component inside my project. I have an Activity with 3 fragments that load perfectly and do everything they need to do. The problem is with one fragment that doesn't load its content when its returned to from backstack.
I'm using navigate functions declared in the ViewModel with Directions from the Navigation.
example ( vm.navigate(SomeFragmentDirections.actionSomeFragmentToOtherFragment)
A(activity) -> B(fragment) -> C(fragment) -> D(fragment)
when I press back on the D fragmentto go back to the C fragment it shows the upper navbar but doesn't load the content of it. I use the same principles on all other Activities/Fragments in my other projects (even in this one) and i don't get that problem. All lifecycle functions are called and everything should work fine. The logcat doesn't show any errors whatsoever. If anyone knows anything about this I would appreciate it.
EDIT:
This is the fragment that doesn't load (Fragment C)
Fragment D is the webView fragment, fragment C navigates to it in the
vm.navigate(RegisterFragmentDirections.actionRegisterFragmentToWebViewFragment(webURL)) function
class RegisterFragment : BaseFragment() {
private val vm: RegisterViewModel by viewModel()
override fun getViewModel(): BaseViewModel = vm
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentRegisterBinding.inflate(inflater, container, false)
context ?: return binding.root
injectFeature()
setToolbar(binding)
subscribeUi(binding)
return binding.root
}
/**
* set toolbar
* **/
private fun setToolbar(binding: FragmentRegisterBinding) {
if (activity is WelcomeActivity) {
binding.appBarLayout.backClickListener = (activity as WelcomeActivity).createOnBackClickListener()
} else if(activity is LoginActivity) {
binding.appBarLayout.backClickListener = (activity as LoginActivity).createOnBackClickListener()
}
}
/**
* set ui
* **/
private fun subscribeUi(binding: FragmentRegisterBinding) {
// set bindings
binding.contentRegister.viewModel = vm
binding.contentSuccess.viewOwner = this
// set true full screen
(activity as LoginActivity).setFullScreen(false)
// set dark status bar icons
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
activity!!.window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
// set initial margin top
ViewCompat.setOnApplyWindowInsetsListener(binding.rootLayout) { _, insets ->
binding.appBarLayout.toolbar.setMarginTop(insets.systemWindowInsetTop)
insets
}
// set phone number mask listener
binding.contentRegister.etPhoneNumber.addTextChangedListener(PhoneNumberFormattingTextWatcher())
// set licence agreement formatted text with hyperlinks
setTextViewHTML(binding.contentRegister.licenceAgreement, getString(R.string.description_privacy_with_link))
// set listener on form elements, error handling
binding.contentRegister.etName.onFocusChangeListener = emptyInputValidationListener(
binding.contentRegister.etName,
binding.contentRegister.tilName,
getString(R.string.error_empty_name)
)
binding.contentRegister.etLastName.onFocusChangeListener = emptyInputValidationListener(
binding.contentRegister.etLastName,
binding.contentRegister.tilLastName,
getString(R.string.error_empty_last_name)
)
binding.contentRegister.etBirthDate.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
when(isBirthDateValid(binding.contentRegister.etBirthDate.text.toString())) {
false -> binding.contentRegister.tilBirthDate.error = getString(R.string.error_date_not_valid)
true -> binding.contentRegister.tilBirthDate.isErrorEnabled = false
}
}
}
binding.contentRegister.etEmail.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
when(!android.util.Patterns.EMAIL_ADDRESS.matcher(binding.contentRegister.etEmail.text!!.trim()).matches()) {
true -> binding.contentRegister.tilEmail.error = getString(R.string.error_email_not_valid)
false -> binding.contentRegister.tilEmail.isErrorEnabled = false
}
}
}
binding.contentRegister.etPassword.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
when (binding.contentRegister.etPassword.text!!.trim().length < 6) {
true -> binding.contentRegister.tilPassword.error =
getString(R.string.error_password_not_valid)
false -> binding.contentRegister.tilPassword.isErrorEnabled = false
}
}
}
binding.contentRegister.registerButton.setOnClickListener{
validateInputs(binding)
}
// set observables
vm.userResponse.observe(viewLifecycleOwner, Observer { updateRegisterSuccess(binding, it) })
}
/**
* update on success / failure
* **/
private fun updateRegisterSuccess(
binding: FragmentRegisterBinding,
resource: Resource<BaseResponseEntity>?
) {
resource?.let {
when (it.state) {
ResourceState.LOADING -> {
binding.contentProgress.isLoading = true
setViewAndChildrenEnabled(binding.rootLayout, false)
}
ResourceState.SUCCESS -> {
binding.contentProgress.isLoading = false
setViewAndChildrenEnabled(binding.rootLayout, true)
}
ResourceState.ERROR -> {
binding.contentProgress.isLoading = false
setViewAndChildrenEnabled(binding.rootLayout, true)
}
}
it.data?.let {
when(it.responseCode) {
RESPONSE_CODE_SUCCESS -> {
binding.contentSuccess.isSucceeded = true
setViewAndChildrenEnabled(binding.rootLayout, true)
}
RESPONSE_CODE_ERROR -> {
if (it.message.isNotEmpty()) {
showSnackbar(it.message, Snackbar.LENGTH_SHORT)
} else {
showSnackbar(getString(R.string.error_unknown), Snackbar.LENGTH_SHORT)
}
}
}
}
it.message?.let {
showSnackbar(getString(R.string.error_unknown), Snackbar.LENGTH_SHORT)
}
}
}
/**
* disable ui elements while loading
* **/
private fun setViewAndChildrenEnabled(view: View, enabled: Boolean) {
view.isEnabled = enabled
if (view is ViewGroup) {
for (i in 0 until view.childCount) {
val child = view.getChildAt(i)
setViewAndChildrenEnabled(child, enabled)
}
}
}
/**
* validate all inputs
* **/
private fun validateInputs(binding: FragmentRegisterBinding) {
// check if all inputs are valid
if(binding.contentRegister.etName.text!!.trim().isEmpty()) {
binding.contentRegister.etName.requestFocus()
binding.contentRegister.tilName.error = getString(R.string.error_empty_name)
return
}
if(binding.contentRegister.etLastName.text!!.trim().isEmpty()) {
binding.contentRegister.etLastName.requestFocus()
binding.contentRegister.tilLastName.error = getString(R.string.error_empty_last_name)
return
}
if (binding.contentRegister.etBirthDate.rawText.isNotEmpty()) {
if (!isBirthDateValid(binding.contentRegister.etBirthDate.text.toString())) {
binding.contentRegister.etBirthDate.requestFocus()
binding.contentRegister.tilBirthDate.error =
getString(R.string.error_date_not_valid)
return
}
}
if(!android.util.Patterns.EMAIL_ADDRESS.matcher(binding.contentRegister.etEmail.text!!.trim()).matches()) {
binding.contentRegister.etEmail.requestFocus()
binding.contentRegister.tilEmail.error = getString(R.string.error_date_not_valid)
return
}
if(binding.contentRegister.etPassword.text!!.trim().length < PASSWORD_MINIMUM_LENGHT) {
binding.contentRegister.etPassword.requestFocus()
binding.contentRegister.tilPassword.error = getString(R.string.error_password_not_valid)
return
}
if(!binding.contentRegister.checkBox.isChecked) {
showSnackbar(getString(R.string.error_terms_and_conditions), Snackbar.LENGTH_SHORT)
return
}
// handle date of birth
val dateOfBirth = if (binding.contentRegister.etBirthDate.rawText.trim().isNotEmpty()
&& isBirthDateValid(binding.contentRegister.etBirthDate.rawText)) {
binding.contentRegister.etBirthDate.text.toString().replace("/", "-")
} else {
""
}
binding.rootLayout.hideKeyboard()
vm.register(
username = binding.contentRegister.etEmail.text.toString(),
password = binding.contentRegister.etPassword.text.toString(),
name = binding.contentRegister.etName.text.toString(),
lastName = binding.contentRegister.etLastName.text.toString(),
phoneNumber = binding.contentRegister.etPhoneNumber.text.toString(),
dateOfBirth = dateOfBirth)
Timber.d(dateOfBirth)
}
//todo handle this and move to util class
#Suppress("DEPRECATION")
private fun setTextViewHTML(text: TextView, html: String) {
// replace \n new line so android can show new line for text which we previously fetchCompanies from server
val hmtlFormatted = html.replace("\n", "<br>")
val sequence = Html.fromHtml(hmtlFormatted)
val strBuilder = SpannableStringBuilder(sequence)
val urls = strBuilder.getSpans(0, sequence.length, URLSpan::class.java)
for (span in urls) {
makeLinkClickable(strBuilder, span)
}
text.text = strBuilder
text.movementMethod = LinkMovementMethod.getInstance()
}
private fun makeLinkClickable(strBuilder: SpannableStringBuilder, span: URLSpan) {
val start = strBuilder.getSpanStart(span)
val end = strBuilder.getSpanEnd(span)
val flags = strBuilder.getSpanFlags(span)
val clickable = object : ClickableSpan() {
override fun onClick(view: View) {
// Do something with span.getURL() to handle the link click...
val webURL = span.url
vm.navigate(RegisterFragmentDirections.actionRegisterFragmentToWebViewFragment(webURL))
}
}
strBuilder.setSpan(clickable, start, end, flags)
strBuilder.removeSpan(span)
}
// PUBLIC ACTIONS ---
fun onRegisterDoneClick() {
// navigate to welcome activity and finish it
onRegisterSuccess()
}
/**
* on register success
* **/
private fun onRegisterSuccess() {
// navigate to welcome activity and finish it
val returnIntent = Intent()
(activity as LoginActivity).setResult(Activity.RESULT_OK, returnIntent)
(activity as LoginActivity).finish()
}
You only get a context once the fragment is attached to an activity.
When onCreateView is called you don't have a context yet and it returns:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentRegisterBinding.inflate(inflater, container, false)
context ?: return binding.root
// ...
}
You should move your set up logic to onViewCreated:
lateinit var binding: FragmentRegisterBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentRegisterBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
injectFeature()
setToolbar(binding)
subscribeUi(binding)
}
I'm attempting to set my search box to not return any search results when the query is empty - i.e. when nothing has been typed in the box. Algolia InstantSearch by default returns all entries to scroll through which are then filtered as the user searches.
I followed the API docs on aloglia's website for removing the empty query but my search box still returns all entries. I'm a little stuck since it seems to be a very straightforward class, but using the default SearchBoxView vs my amended version NoEmptySearchBox makes no difference in results.
Here's GroupFragment where I'm calling the SearchBoxView:
class GroupFragment : Fragment() {
private val connection = ConnectionHandler()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.group_fragment, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val viewModel = ViewModelProviders.of(requireActivity())[SearcherViewModel::class.java]
val searchBoxView = NoEmptySearchBox(searchView)
viewModel.groups.observe(this, Observer { hits -> viewModel.adapterProduct.submitList(hits) })
connection += viewModel.searchBox.connectView(searchBoxView)
groupList.let {
it.itemAnimator = null
it.adapter = viewModel.adapterProduct
it.layoutManager = LinearLayoutManager(requireContext())
it.autoScrollToStart(viewModel.adapterProduct)
}
}
override fun onDestroyView() {
super.onDestroyView()
connection.disconnect()
}
}
And here's my NoEmptySearchBox class, which implements SearchBoxView:
class NoEmptySearchBox (
val searchView: SearchView
) : SearchBoxView {
override var onQueryChanged: Callback<String?>? = null
override var onQuerySubmitted: Callback<String?>? = null
init {
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
query?.isNotEmpty()?.let { onQuerySubmitted?.invoke(query) }
return false
}
override fun onQueryTextChange(query: String?): Boolean {
query?.isNotEmpty()?.let { onQuerySubmitted?.invoke(query) }
return false
}
})
}
override fun setText(text: String?, submitQuery: Boolean) {
searchView.setQuery(text, false)
}
}
And here's my SearcherViewModel:
class SearcherViewModel : ViewModel() {
val client = ClientSearch(ApplicationID("APP_ID"), APIKey("API_KEY"), LogLevel.ALL)
val index = client.initIndex(IndexName("groups"))
val searcher = SearcherSingleIndex(index)
override fun onCleared() {
super.onCleared()
searcher.cancel()
connection.disconnect()
}
val dataSourceFactory = SearcherSingleIndexDataSource.Factory(searcher) { hit ->
Group(
hit.json.getPrimitive("course_name").content,
hit.json.getObjectOrNull("_highlightResult")
)
}
val pagedListConfig = PagedList.Config.Builder().setPageSize(50).build()
val groups: LiveData<PagedList<Group>> = LivePagedListBuilder(dataSourceFactory, pagedListConfig).build()
val adapterProduct = GroupAdapter()
val searchBox = SearchBoxConnectorPagedList(searcher, listOf(groups))
val connection = ConnectionHandler()
init {
connection += searchBox
}
}