How to use livedata, do setValue in activity, and read observe in a fragment, is it possible at all? I know that with this code it calls a new instance twice, but how to do it correctly?
viewmodel:
public class PongViewModel extends ViewModel {
private MutableLiveData<String> pongSections;
public MutableLiveData<String> getPongSections() {
if (pongSections == null) {
pongSections = new MutableLiveData<String>();
}
return pongSections;
}
}
MainAcitivity:
// OnCreate
pongViewModel = new ViewModelProvider(this).get(PongViewModel.class);
pongViewModel.getPongSections().setValue("test");
Fragment:
pongViewModel = new ViewModelProvider(this).get(PongViewModel.class);
pongViewModel.getPongSections().observe(this, pongSections -> {
System.out.println("DATA !!!");
});
Use a viewmodel scoped to the activity to get the viewmodel from the activity, since at the moment you are right, you created a new viewmodel instance:
class YourFragment extends Fragment {
private val pongViewModel: PongViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//This should fire after updating in the activity now
pongViewModel.getPongSections().observe(this, pongSections -> {
System.out.println("DATA !!!");
});
}
}
Documentation source
Related
I have hideKeyboard(view: View) method in util class. In my activity I want to call that method. I created object in activity utils = Utils() and then utils.HideKeyboard(binding.authConstraint) but when Im trying to click on constraintLayout it is throwing error "System services not available to Activities before onCreate()" what am I doing wrong ?
my Util Class
open class Utils():Activity(){
fun hideKeyboard(view: View) {
val inputMethodManager =
getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
}
my activity
class AuthActivity : ActivityWithDisposableList() {
private lateinit var binding: ActivityAuthBinding
private lateinit var authViewModel: AuthViewModel
val utils = Utils()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_auth)
authViewModel = ViewModelProviders.of(this).get(AuthViewModel::class.java)
binding.authViewModel = authViewModel
binding.lifecycleOwner = this
authViewModel.isKeyboardClosed.observe(this, Observer { isTrue ->
if (isTrue) {
utils.hideKeyboard(binding.authConstraint,this)
binding.usernameInputEditText.clearFocus()
binding.passwordInputEditText.clearFocus()
}
})
}
}
Do not create an instance of an Activity subclass yourself. And do not make some arbitrary class extend Activity, just because you need a Context. Get the Context from a parameter to a constructor or function.
With that in mind, replace your Utils with:
class Utils {
fun hideKeyboard(view: View) {
val inputMethodManager =
view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
}
}
Here, you are getting the Context from the View that you are supplying.
Note that you could also go with object Utils or have hideKeyboard() be an extension function on View, if you liked.
My question is about Mapbox. In this period I am working on an ANDROID application based on mapbox, using Kotlin and Fragments and my problem concerns the visualization of points on the map itself. That is my need is to be able to show points on the map through a GEOJSON file, for now I have been able to see the map in full in the application, but I cannot find a way to show the points taken from a GeoJson file and locate myself in the map via a button.
I should implement both functions in the fragment, so my problem is precisely that of not being able to show the points of a geojson file and find a way to locate myself in the map itself. I await help if there is someone able to help me with this problem, I also leave the code of the fragment class in kotlin.
Thanks everyone in advance !!
FRAGMENT HOME
class HomeFragment : Fragment() {
private var mapView: MapView? = null
#Nullable
override fun onCreateView(
inflater: LayoutInflater,
#Nullable container: ViewGroup?,
#Nullable savedInstanceState: Bundle?
): View? {
Mapbox.getInstance(
context!!.applicationContext,
"MIO CODICE MAPBOX"
)
val view: View = inflater.inflate(R.layout.fragment_home, container, false)
mapView = view.findViewById<View>(R.id.mapView) as MapView
mapView!!.onCreate(savedInstanceState)
return view
}
override fun onResume() {
super.onResume()
mapView!!.onResume()
}
override fun onPause() {
super.onPause()
mapView!!.onPause()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
mapView!!.onSaveInstanceState(outState)
}
override fun onLowMemory() {
super.onLowMemory()
mapView!!.onLowMemory()
}
override fun onDestroyView() {
super.onDestroyView()
mapView!!.onDestroy()
}
}
HOST ACTIVITY :
class HostActivity : AppCompatActivity() {
lateinit var googleSignInClient: GoogleSignInClient
private lateinit var navController: NavController
private val mAuth: FirebaseAuth = FirebaseAuth.getInstance()
private val db: FirebaseFirestore = FirebaseFirestore.getInstance()
private lateinit var drawerLayout: DrawerLayout
private lateinit var navViewBinding: DrawerHeaderLayoutBinding
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.AppTheme)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_host)
val toolbar = customToolbar
setSupportActionBar(toolbar)
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(getString(R.string.default_web_client_id))
.requestEmail()
.build()
googleSignInClient = GoogleSignIn.getClient(this, gso)
drawerLayout = drawer_layout
navViewBinding = DrawerHeaderLayoutBinding.inflate(layoutInflater, navView, true)
val navHost =
supportFragmentManager.findFragmentById(R.id.navHostFragment) as NavHostFragment
navController = navHost.navController
val navInflater = navController.navInflater
val graph = navInflater.inflate(R.navigation.main_graph)
navController.addOnDestinationChangedListener { _, destination, _ ->
if (destination.id == R.id.onBoarding ||
destination.id == R.id.authFragment ||
destination.id == R.id.loginFragment ||
destination.id == R.id.signUpFragment
) {
toolbar.visibility = View.GONE
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
} else {
toolbar.visibility = View.VISIBLE
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
}
}
if (!Prefs.getInstance(this)!!.hasCompletedWalkthrough!!) {
if (mAuth.currentUser == null) {
graph.startDestination = R.id.authFragment
} else {
getUserData()
graph.startDestination = R.id.homeFragment
}
} else {
graph.startDestination = R.id.onBoarding
}
navController.graph = graph
NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout)
navView.setupWithNavController(navController)
navView.setNavigationItemSelectedListener {
it.isChecked
drawerLayout.closeDrawers()
when (it.itemId) {
R.id.action_logout -> {
MyApplication.currentUser!!.active = false
FirestoreUtil.updateUser(MyApplication.currentUser!!) {
mAuth.signOut()
}
googleSignInClient.signOut()
MyApplication.currentUser = null
navController.navigate(R.id.action_logout)
}
}
true
}
}
private fun getUserData() {
val ref = db.collection("users").document(mAuth.currentUser!!.uid)
ref.get().addOnSuccessListener {
val userInfo = it.toObject(UserModel::class.java)
navViewBinding.user = userInfo
MyApplication.currentUser = userInfo
MyApplication.currentUser!!.active = true
FirestoreUtil.updateUser(MyApplication.currentUser!!) {
}
}.addOnFailureListener {
val intent = Intent(this, MyApplication::class.java)
startActivity(intent)
finish()
}
}
override fun onSupportNavigateUp(): Boolean {
return NavigationUI.navigateUp(navController, drawerLayout)
}
}
You'll need to use a GeoJsonSource. https://github.com/mapbox/mapbox-android-demo/search?q=GeoJsonSource shows how the demo app uses the source.
https://github.com/mapbox/mapbox-android-demo/blob/master/MapboxAndroidDemo/src/main/java/com/mapbox/mapboxandroiddemo/examples/basics/KotlinSupportMapFragmentActivity.kt (Its XML layout file)
Putting icons on the map. https://docs.mapbox.com/android/maps/examples/marker-symbol-layer/. Do all of the icon setup inside of the fragment's onStyleLoaded() callback as seen at https://github.com/mapbox/mapbox-android-demo/blob/master/MapboxAndroidDemo/src/main/java/com/mapbox/mapboxandroiddemo/examples/basics/KotlinSupportMapFragmentActivity.kt#L51-L55
https://github.com/mapbox/mapbox-android-demo/search?q=loadGeojson shows how the demo app loads from a GeoJson file. You could use coroutines instead of building out the AsyncTask.
Although it's in Java, https://docs.mapbox.com/android/maps/examples/show-a-users-location-on-a-fragment/ shows how to combine the Maps SDK's LocationComponent with a fragment.
Regarding moving the camera to the device's last known location when a button is clicked, see my answer at https://stackoverflow.com/a/64159178/6358488
I have a RecyclerView implemented with the Groupie library and I can delete an item from the list fine, however need to update the view to see the change. I'd like to have something like notifyDataSetChanged() instead, so the list updates immediately. I'm a bit confused at this stage though, tried a few different ways to get an interface from the class that hosts my view holder to be triggered from the fragment that holds the adapter but I think I'm stuck now if I could get some help please.
class RecyclerProductItem(
private val activity: MainActivity,
private val product: Product, private val adapterListener: AdapterListener
) : Item<GroupieViewHolder>() {
companion object {
var clickListener: AdapterListener? = null
}
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
viewHolder.apply {
with(viewHolder.itemView) {
clickListener = adapterListener
ivTrash.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View?) {
if (clickListener != null) {
Toast.makeText(context, "delete method to be added here", Toast.LENGTH_SHORT).show()
clickListener?.onClickItem(position)
}
}
})
}
}
}
override fun getLayout() = R.layout.recyclerview_item_row
interface AdapterListener {
fun onClickItem(position: Int)
}
}
Here it's my fragment. I tried to add a section to the adapter to see if it would allow me to retrieve a listener for it, but as my listener should be triggered under a specific item within the layout, this may not be the best solution, although couldn't make this work either.
class ProductsListFragment : Fragment(), RecyclerProductItem.AdapterListener {
private lateinit var adapter: GroupAdapter<GroupieViewHolder>
private val section = Section()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_products_list, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val linearLayoutManager = LinearLayoutManager(activity)
recyclerView.layoutManager = linearLayoutManager
adapter = GroupAdapter()
adapter.add(section)
recyclerView.adapter = adapter
loadProducts()
}
private fun loadProducts() {
GetProductsAPI.postData(object : GetProductsAPI.ThisCallback {
override fun onSuccess(productList: List<JsonObject>) {
for (jo in productList) {
val gson = GsonBuilder().setPrettyPrinting().create()
val product: Product =
gson.fromJson(jo, Product::class.java)
adapter.add(
RecyclerProductItem(
activity as MainActivity,
Product(
product.id,
product.title,
product.description,
product.price
),adapterListenerToBePassedHere
)
) // This part is where I should be giving the listener, but get a red line since not sure how to get it to include it here.
}
}
})
}
companion object {
fun newInstance(): ProductsListFragment {
return ProductsListFragment()
}
}
override fun onClickItem(position: Int) {
adapter.notifyItemRemoved(position)
}
}
Many thanks.
I think you are missing this concept from the groupie Readme:
Modifying the contents of the GroupAdapter in any way automatically sends change notifications. Adding an item calls notifyItemAdded(); adding a group calls notifyItemRangeAdded(), etc.
So to remove an item, call section.remove(item). However, in your onClickItem function you currently only pass the position. Pass the item like clickListener?.onClickItem(this#RecyclerProductItem) instead.
Even more ideally and safely you should remove by product.id, e.g. clickListener?.onClickItem(this#RecyclerProductItem.product.id) then in onClickItem() you just search for the item with that product id and remove it. Let me know if I'm not clear.
Based on #carson's reply, this is what worked for me. Had to add the items to the section, the section to the adapter and then remove the item from the section based on the adapter position once that listener is clicked, passing the method that implements the listener as one of the arguments to complete the GroupAdapter.
class RecyclerProductItem(
private val activity: MainActivity,
private val product: Product, private val adapterListener: AdapterListener
) : Item<GroupieViewHolder>() {
companion object {
var clickListener: AdapterListener? = null
}
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
viewHolder.apply {
with(viewHolder.itemView) {
tvTitle.text = product.title
clickListener = adapterListener
ivTrash.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View?) {
if (clickListener != null) {
Toast.makeText(context, "delete method to be added here", Toast.LENGTH_SHORT).show()
clickListener?.onClickItem(this#RecyclerProductItem.product.id, adapterPosition)
}
}
})
}
}
}
override fun getLayout() = R.layout.recyclerview_item_row
interface AdapterListener {
fun onClickItem(id: Int, position: Int)
}
}
And
private fun loadProducts() {
GetProductsAPI.postData(object : GetProductsAPI.ThisCallback,
RecyclerProductItem.AdapterListener {
override fun onSuccess(productList: List<JsonObject>) {
Log.i(LOG_TAG, "onSuccess $LOG_TAG")
for (jo in productList) {
val gson = GsonBuilder().setPrettyPrinting().create()
val product: Product =
gson.fromJson(jo, Product::class.java)
val linearLayoutManager = LinearLayoutManager(activity)
recyclerView.layoutManager = linearLayoutManager
adapter = GroupAdapter()
section.add(
RecyclerProductItem(
activity as MainActivity,
Product(
product.id,
product.title,
product.description,
product.price
), this
)
)
adapter.add(section)
recyclerView.adapter = adapter
}
}
override fun onFailure() {
Log.e(LOG_TAG, "onFailure $LOG_TAG")
}
override fun onError() {
Log.e(LOG_TAG, "onError $LOG_TAG")
}
override fun onClickItem(id: Int, position: Int) {
section.remove(adapter.getItem(position))
}
})
}
I'm trying to make something like a timed session when a user logs in in the app. I want to set up a timer which, when finished, change the activity with an intent, going back to the login page. I'm new to Android programming and I don't know how something like this should be implemented.
What I tried so far (and currently is working as intended, I just don't know if it's the correct thing to do, because it seems odd to have a function from another activity running in the background like this) is:
Countdowntimer is setted up in the login activity.
When the "login" button is pressed the timer starts.
OnTick() changes the activity with an intent.
OnFinish() is setted up in the login activity and, when it runs, it changes activity with an intent going back to the login page, even if the user is on a completely different activity.
As I said before, this works just fine, it just seems strange and I can't find anything that tells how this should work.
Using Intent Service Start your Timer & Add Event Bus Dependency to your project.
Event Bus
public class TimerIntentService extends IntentService {
public TimerIntentService() {
super("TimerIntentService");
}
public static void startTimer(Context context) {
Intent intent = new Intent(context, TimerIntentService.class);
context.startService(intent);
}
private CountDownTimer countdownTimer = new CountDownTimer(60000,1000) {
#Override
public void onTick(long millisUntilFinished) {
EventBus.getDefault().postSticky(new MessageEventToActivityEvent(TimerStatus.RUNNING));
}
#Override
public void onFinish() {
EventBus.getDefault().postSticky(new MessageEventToActivityEvent(TimerStatus.FINISHED));
}
};
#Override
protected void onHandleIntent(Intent intent) {
EventBus.getDefault().postSticky(new MessageEventToActivityEvent(TimerStatus.STARTED));
countdownTimer.start();
}
#Override
public void onDestroy() {
super.onDestroy();
try{
countdownTimer.cancel();
} catch (Exception e){
e.printStackTrace();
}
stopSelf();
}}
step 2:
in your first activity start intent service
TimerIntentService.startTimer(context);
step 3:
in your base activity or second activity add like the following
public class TimerBaseActivity extends AppCompatActivity {
#Subscribe(threadMode = ThreadMode.MAIN,sticky=true)
public void onMessageEvent(MessageEventToActivity event) {
if(event.timerStatus!=null){
if(event.timerStatus==TimerStatus.FINISHED){
// do your Stuff
}
if(event.timerStatus.equals(TimerStatus.RUNNING)){
Log.e("Timer State : ",""+event.timerStatus.name());
}
}
}
#Override
public void onStart() {
super.onStart();
EventBus.getDefault().register(this);
}
#Override
public void onStop() {
super.onStop();
EventBus.getDefault().unregister(this);
}}
With Koin DI & MVVM It is a very simple one.
Step 1: define your timer in your ViewModel
class TimerViewModel(application: Application) : AndroidViewModel(application) {
val isFinished = MutableLiveData<Boolean>().apply { value = false }
val timerTask: CountDownTimer = object : CountDownTimer(60000, 1000) {
override fun onFinish() {
Log.e("CountDownTimer","onFinish")
isFinished.postValue(true)
}
override fun onTick(millisUntilFinished: Long) {
Log.e("millisUntilFinished","$millisUntilFinished")
}
}}
step 2: Add your model as a singleton using Koin
val myModule = module {
single { TimerViewModel(androidApplication()) }}
Step 3: start your time in Your first activity
class MainActivity : AppCompatActivity() {
lateinit var activityMainBinding: ActivityMainBinding
val timerViewModel: TimerViewModel by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
activityMainBinding.viewModel = timerViewModel
activityMainBinding.lifecycleOwner = this
timerViewModel.timerTask.start()
startActivity(Intent(this, SecondActivity::class.java))
}
Step 4: Observe ViewModeldata in Second Activity
class SecondActivity : AppCompatActivity() {
val timerViewModel: TimerViewModel by inject()
lateinit var activitySecondBinding: ActivitySecondBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activitySecondBinding = DataBindingUtil.setContentView(this, R.layout.activity_second)
activitySecondBinding.viewModel = timerViewModel
activitySecondBinding.lifecycleOwner = this
timerViewModel.isFinished.observe(this, Observer {isTimerFinished->
if(isTimerFinished){
Toast.makeText(this#SecondActivity,"Timer Finished",Toast.LENGTH_LONG).show()
}
})
}
fun stopTimer(view:View){
timerViewModel.timerTask.cancel()
}
}
Note:
Problem Solved. This is one of the ways. If you don't want to use singleton object, then run your timer in IntentService. onFinish notify to the activity using EventBus. Event Bus will share your event with multiple activities.
I have two screens included in my app. I am returning a list of ads after parsing xml and making an api call. Then whenever I click on a recyclerView Item another activity is displayed showing more data about the clicked item. Whenever I press the back button, a new api call is issued. So how to prevent that?
Any Suggestion?
Here is my Main Activity:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Setting the adapter and the recycler view
loadPage();
}
public void loadPage() {
new DownloadXmlTask().execute(URL);
}
private class DownloadXmlTask extends AsyncTask<String, Void, List<AdEntry>> {
#Override
protected List<AdEntry> doInBackground(String... urls) {
try {
return loadXmlFromNetwork(urls[0]);
} catch (IOException e) {
return null;
} catch (XmlPullParserException e) {
return null;
}
}
#Override
protected void onPostExecute(List<AdEntry> adEntries) {
Toast.makeText(MainActivity.this, "Successfully Processed", Toast.LENGTH_SHORT).show();
mAdsAdapter.setAdData(adEntries);
entryList = adEntries;
super.onPostExecute(adEntries);
}
}
public List<AdEntry> loadXmlFromNetwork(String urlString) throws XmlPullParserException, IOException {
InputStream stream = null;
try {
stream = downloadUrl(urlString);
entryList = parse(stream);
} finally {
if (stream != null)
stream.close();
}
return entryList;
}
public InputStream downloadUrl(String urlString) throws IOException {
// Open HttpURLConnection
}
#Override
public void onClick(View view, int position) {
//Some code
}
In which lifecycle method are you making your api call?
onCreate for example would be the method in which you could make your api call, that method is only called once in the activity's lifecycle (i.e. when your activity is created or recreated)
onStart and onResume will be called when your activity becomes visible again, I guess this is what's happening in your case.
Handling the back press can be done by overriding onBackPressed, but that won't help much with your problem without writing quite a bit of code that isn't necessary.
So, try to make your api call in onCreate.
You are calling your api in a right place.
I guess that the problem is finishing the mainActivity and then go to second activity.
so if you need to use the back button and come back to the mainActivity, you don't have to finish the mainActivity and by this approach the onCreate will not trigger again.
and check the activity lifecycle here activity lifecycle
make sure that you check first how the Android lifecycle works (https://developer.android.com/guide/components/activities/activity-lifecycle). It will help you decide where to call the API in the first place (onCreate could be the place to do this)
When you return to a previous activity, it will most commonly invoke the onResume method.
You might also want to consider saving the state of the activity, removing the necessity to call the API again, or consider a Cache to avoid calling the API multiple times.
== UPDATE ==
A. Change the launch mode of your app to be a Single Instance
<activity android:name=".MainActivity"
android:launchMode="singleInstance">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".DetailActivity"
android:label="#string/title_activity_detail"
android:parentActivityName=".MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity"/>
</activity>
B. Configure your adapter and view in the onCreate. Initiate the task, etc etc
package com.example.mylist
import // ... all your imports
class MainActivity : AppCompatActivity(), BookLoadingTaskDelegate {
private var listView: ListView? = null
private var data: MutableList<Book> = mutableListOf()
private lateinit var adapter: BooksAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
this.listView = findViewById(R.id.listView)
this.adapter = BooksAdapter(this, data)
loadNetworkIntensiveData()
}
override fun onResume() {
super.onResume()
this.listView!!.adapter = this.adapter
}
private fun loadNetworkIntensiveData() {
LoadingTask(this).execute()
}
override fun processResult(result: List<Book>?) {
Log.d("ProcessResult", "Found ${result?.size}")
this.listView!!.post {
data.addAll(result!!)
Log.d("ProcessResult", "Found data elements: *${data.size}")
adapter.notifyDataSetChanged()
}
this.listView!!.onItemClickListener = AdapterView.OnItemClickListener { parent, view, position, id ->
val intent = Intent(this, DetailActivity::class.java)
startActivity(intent)
}
}
class LoadingTask(private val delegate: BookLoadingTaskDelegate) : AsyncTask<Void, Void, List<Book>>() {
override fun doInBackground(vararg params: Void?): List<Book> {
Log.w("Background task", "Loading heavy stuff in the background")
val tempList: MutableList<Book> = mutableListOf()
// ... Load data into the list
return tempList
}
override fun onPostExecute(result: List<Book>?) {
delegate.processResult(result)
}
}
data class Book(
val title: String,
val summary: String
)
class BooksAdapter(private val context: Context, private val data: List<Book>) : BaseAdapter() {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
Log.d("GetView", "Retrieving element at position $position")
val book = getItem(position) as Book
var v = //... do what you need to do to populate the view
return v
}
override fun getCount(): Int {
Log.d("Adapter - Get Count", "Size: ${data.size}")
return data.size
}
override fun getItem(position: Int): Any? {
return if (position > data.size) null else data[position]
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
}
}
interface BookLoadingTaskDelegate {
fun processResult(result: List<MainActivity.Book>?)
}