I'm using Android's Navigation component and I'm wondering how to setup AlertDialog from a fragment with a click listener.
MyFragment
fun MyFragment : Fragment(), MyAlertDailog.MyAlertDialogListener {
...
override fun onDialogPostiveCLick(dialog: DialogFragment) {
Log.i(TAG, "Listener returns a postive click")
}
fun launchMyAlertDialog() {
// Here I would typically call setTargetFragment() and then show the dialog.
// but findnavcontroller doesn't have setTargetFragment()
findNavController.navigate(MyFragmentDirection.actionMyFragmentToMyAlertDialog())
}
}
MyAlertDialog
class MyAlertDialog : DialogFragment() {
...
internal lateinit var listener: MyAlertDialogListener
interface MyAlertDialogListener{
fun onDialogPostiveCLick(dialog: DialogFragment)
}
override fun onCreateDialog(savdInstanceState: Bundle?): Dialog {
return activity?.let {
val builder = AlertDialog.Builder(it)
builder.setMessage("My Dialog message")
.setPositiveButton("Positive", DialogInterface.OnClickListener {
listener = targetFragment as MyAlertDialogListener
listener.onDialogPositiveClick(this)
}
...
}
}
}
This currently receives a null point exception when initializing the listener in MyAlertDialog.
To use targetFragment, you have to set it first as you commented, unfortunately jetpack navigation does not do this for you (hence the null pointer exception). Check out this thread for alternative solution: https://stackoverflow.com/a/50752558/12321475
What I can offer you is an alternative. If the use-case is as simple as displaying a dialog above current fragment, then do:
import androidx.appcompat.app.AlertDialog
...
class MyFragment : Fragment() {
...
fun onDialogPostiveCLick() {
Log.i(TAG, "Listener returns a postive click")
}
fun launchMyAlertDialog() {
AlertDialog.Builder(activity)
.setMessage("My Dialog message")
.setPositiveButton("Positive") { _, _ -> onDialogPostiveCLick() }
.setCancellable(false)
.create().show()
}
}
Related
I am new to kotlin and jetpack, I am requested to handle errors (exceptions) coming from the PagingData, I am not allowed to use Flow, I am only allowed to use LiveData.
This is the Repository:
class GitRepoRepository(private val service: GitRepoApi) {
fun getListData(): LiveData<PagingData<GitRepo>> {
return Pager(
// Configuring how data is loaded by adding additional properties to PagingConfig
config = PagingConfig(
pageSize = 20,
enablePlaceholders = false
),
pagingSourceFactory = {
// Here we are calling the load function of the paging source which is returning a LoadResult
GitRepoPagingSource(service)
}
).liveData
}
}
This is the ViewModel:
class GitRepoViewModel(private val repository: GitRepoRepository) : ViewModel() {
private val _gitReposList = MutableLiveData<PagingData<GitRepo>>()
suspend fun getAllGitRepos(): LiveData<PagingData<GitRepo>> {
val response = repository.getListData().cachedIn(viewModelScope)
_gitReposList.value = response.value
return response
}
}
In the Activity I am doing:
lifecycleScope.launch {
gitRepoViewModel.getAllGitRepos().observe(this#PagingActivity, {
recyclerViewAdapter.submitData(lifecycle, it)
})
}
And this is the Resource class which I created to handle exceptions (please provide me a better one if there is)
data class Resource<out T>(val status: Status, val data: T?, val message: String?) {
companion object {
fun <T> success(data: T?): Resource<T> {
return Resource(Status.SUCCESS, data, null)
}
fun <T> error(msg: String, data: T?): Resource<T> {
return Resource(Status.ERROR, data, msg)
}
fun <T> loading(data: T?): Resource<T> {
return Resource(Status.LOADING, data, null)
}
}
}
As you can see I am using Coroutines and LiveData. I want to be able to return the exception when it occurs from the Repository or the ViewModel to the Activity in order to display the exception or a message based on the exception in a TextView.
Your GitRepoPagingSource should catch retryable errors and pass them forward to Paging as a LoadResult.Error(exception).
class GitRepoPagingSource(..): PagingSource<..>() {
...
override suspend fun load(..): ... {
try {
... // Logic to load data
} catch (retryableError: IOException) {
return LoadResult.Error(retryableError)
}
}
}
This gets exposed to the presenter-side of Paging as LoadState, which can be reacted to via LoadStateAdapter, .addLoadStateListener, etc as well as .retry. All of the presenter APIs from Paging expose these methods, such as PagingDataAdapter: https://developer.android.com/reference/kotlin/androidx/paging/PagingDataAdapter
You gotta pass your error handler to the PagingSource
class MyPagingSource(
private val api: MyApi,
private val onError: (Throwable) -> Unit,
): PagingSource<Int, MyModel>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, YourModel> {
try {
...
} catch(e: Exception) {
onError(e) // <-- pass your error listener here
}
}
}
im trying to navigate from webview to fragment using navController, but it seems not working.
here is my code
class LandingPageCreditCardFragment : Fragment(R.layout.fragment_landing_page_credit_card) {
private val binding by viewBinding(FragmentLandingPageCreditCardBinding::bind)
lateinit var navController: NavController
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
navController = view.findNavController()
Log.i("link url", arguments?.getString("url").toString())
showWebViewContent(requireArguments().getString("url")!!)
}
private fun showWebViewContent(url: String) {
binding.webViewCreditCard.settings.javaScriptEnabled = true
binding.webViewCreditCard.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
if (url != null) {
view?.loadUrl(url)
}
return true
}
}
binding.webViewCreditCard.addJavascriptInterface(object : Any() {
#JavascriptInterface
fun valid() {
Toast.makeText(requireContext(), "test", Toast.LENGTH_SHORT).show()
val builder = AlertDialog.Builder(requireContext())
builder.setTitle("Konfirmasi")
builder.setMessage("navigasi ke pesanan")
builder.setPositiveButton("Ya") { dialog, which ->
navController.navigate(R.id.action_landingPageCreditCardFragment_to_pesananFragment)
dialog.dismiss()
}
builder.setNegativeButton("Tidak") { dialog, which ->
dialog.dismiss()
}
builder.show() }
}, "btn")
binding.webViewCreditCard.loadUrl(url)
} }
im referrring to this answer to get my function working
i try to show toast, its working
then im try to show alertDialog contain button to navigate from webview to next fragment, when i click it, my app crash.
any help appreciated
after much debugging, i found the answer.
call the navController in runOnUiThread wrapped on Thread and Handler
binding.webViewCreditCard.addJavascriptInterface(object : Any() {
#JavascriptInterface
fun valid() {
val handler = Handler(Looper.getMainLooper())
handler.post(Thread {
(activity as MainActivity).runOnUiThread {
navController.navigate(R.id.action_landingPageCreditCardFragment_to_pesananFragment)
}
})
}
}, "btn")
MyAlertDialog throws ClassCastException when trying to set the context to the listener. I'm calling the MyAlertDailog from a fragment.
I'm using the guide found in the android dev docs.
https://developer.android.com/guide/topics/ui/dialogs#PassingEvents
MyFragment
class MyFragment : Fragment(), MyAlerDialog.MyAlertDialogListener {
...
fun launchAlertDialog() {
val dailog = MyAlertDialog().also {
it.show(requireActivity().supportFragmentManager, "DialogInfoFragment")
}
}
override fun onDialogPostiveCLick(dialog: DialogFragment) {
Log.i(TAG, "Listener returns a postive click")
}
}
MyAlertDialog
class MyAlertDialog : DialogFragment() {
// Use thsi instance for the interface
internal var listener: MyAlertDialogListener
// My Fragment implements this interface.
interface MyAlertDialogListener {
onDialogPositiveClick(dialog: DialogFragment)
}
override fun onAttach(context: Context) {
super.onAttach(context)
// Verify that the host implements the callback.
try {
// My problem is here.
listener = context as MyAlertDailog
} catch (e: ClassCastException) {
// exception thrown here.
throw ClassCastException((context.toString() + " must implement MyAlertDailogListener")
}
}
override fun onCreateDialog(savedInstanceState:Bundle?): Dialog {
return activity?.let {
val builder = AlertDialog.builder(it)
...
builder.setPosiviveButton("Positive button",
DialogInterface.OnClickListener {
listener.onDialogPositiveClick(this)
}
}
}
}
Error report
java.lang.ClassCastException: com.example.androiddevpractice.MainActivity#ab56136 must implement MyAlertDialogListener
at com.example.androiddevpractice.topics.userinterface.dialog.MyAlertDialog.onAttach(MyAlertDialog.kt:35)
Even though the launchAlertDialog() method is inside of MyFragment, the "host" for MyAlertDialog is your Activity, not MyFragment.
Implement MyAlerDialog.MyAlertDialogListener inside of MainActivity in order for the cast to succeed. MainActivity can then communicate to MyFragment if it has to.
Alternatively, you could use setTargetFragment() in order to "connect" MyFragment and MyAlertDialog directly:
val dialog = MyAlertDialog()
dialog.setTargetFragment(this, 0)
dialog.show(requireActivity().supportFragmentManager, "DialogInfoFragment")
Then, rather than overriding onAttach() and casting a context, you would cast the results of getTargetFragment():
builder.setPosiviveButton(
"Positive button",
DialogInterface.OnClickListener {
val listener = targetFragment as MyAlertDialogListener
listener.onDialogPositiveClick(this)
}
)
How do I make calls when I click loan1 button happen after getting a JobCancellationException on other calls ?
class MainRepository{
//called many times
suspend fun getLoanOptions(): Resource<LoanOptionsResponse> {
return try {
val response = apiService.getLoanOptions("id")
responseHandler.handleSuccess(response)
} catch (e: Exception) {
responseHandler.handleException(e)
}
}
}
class MainViewModel : ViewModel(), CoroutineScope {
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
Timber.e("$throwable")
}
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + SupervisorJob() + exceptionHandler
private val mainRepo: MainRepository by lazy { MainRepository() }
//extra calls to this fails
fun getLoanOptions(): LiveData<Resource<LoanOptionsResponse>> {
return liveData(coroutineContext) {
val data = mainRepo.getLoanOptions()
emit(Resource.loading(null))
emit(data)
}
}
override fun onCleared() {
super.onCleared()
coroutineContext.cancel()
}
}
//in mainactivity I call it
class MainActivity : BaseActivity() {
val vm: MainViewModel by lazy { ViewModelProvider(this).get(MainViewModel::class.java) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
loan1.setOnClickListener {
// if any previous api has 403 this one does not work any more ?
vm.getLoanOptions().observe(this, Observer {
//data
}
}
loan2.setOnClickListener {
}
}
}
I clicked on button -> Api is called -> got success response
Again I clicked on -> Api is called -> got success response
I get 403 in other API call
I clicked on button -> Api is not called
No other Api calls gets called in this Activity :(
I get this after few minutes
kotlinx.coroutines.JobCancellationException: Job was cancelled; job=StandaloneCoroutine
When I run the following code
fun main(args: Array<String>) {
Application.launch(HelloWorldApp::class.java, *args)
}
class HelloWorldApp : App(HelloWorld::class)
class HelloWorld : View() {
override val root = hbox {
addEventFilter(KeyEvent.ANY) { event ->
println("pressed:"+event.character)
}
}
}
When I press any keys on my keyboard the println() is never called. Am I missing something?
Simply adding an HBox does not give it focus, and when it doesn't have focus it won't receive key events. You should override onDock and add the listener to the currentScene instead. If you really need to add the listener on the HBox, add the listener and request focus once the view has been docked:
fun main(args: Array<String>) {
launch<HelloWorldApp>(args)
}
class HelloWorldApp : App(HelloWorld::class)
class HelloWorld : View() {
override val root = hbox {
addEventFilter(KeyEvent.ANY) { event ->
println("pressed:" + event.character)
}
}
override fun onDock() {
root.requestFocus()
}
}
Looking for a similar problem I came up with this, which looks simpler, but I do not yet understand any possible subtle differences between using the keyboard control versus explicit focus requesting.
import javafx.scene.input.KeyEvent
import tornadofx.*
fun main(args: Array<String>) {
launch<HelloWorldApp>(args)
}
class HelloWorldApp : App(HelloWorld::class)
class HelloWorld : View() {
override val root = hbox {
keyboard {
addEventHandler(KeyEvent.KEY_PRESSED) { println(it.code) }
}
}
}