Adding support for rich text in Eventyay Organizer App

The Open Event Organizer App provides the users with its one of the core features of the ability to create or update an event. To add this feature, we will use HTML and android’s WebView in order to aid us integrate support for rich text in the app.

The first step is adding the Wasabeef RichText library to your project. Open your build.gradle file and add the support library to the dependency section.

Adding the dependency in build.gradle(app-level) in the project:

dependencies {
   //Other dependencies
   //Rich text editor
   implementation “jp.wasabeef:richeditor-android:1.2.2”
}

What we need is an activity accessible throughout the project for any element that needs to input rich text, which means that this activity goes in the utils package.

Let’s start with building this activity:

We need to first set a hint text or a placeholder text for the editor in case there’s no saved text for the concerned field.

public class RichEditorActivity extends AppCompatActivity {
   

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       

       binding.editor.setPlaceholder(getString(R.string.enter_text));
       Intent intent = getIntent();
       if (intent != null) {
         description = intent.getStringExtra(TAG_RICH_TEXT);
         if (!TextUtils.isEmpty(description) && !description.equals(getString(R.string.describe_event))) {
           binding.editor.setHtml(description);
         }
       }
}

Now let’s see how we add the formatted text to our WebView. Currently we are supporting the options:

  • Undo
  • Redo
  • Bold
  • Italic
  • StrikeThrough
  • Bulleted list
  • Numbered list

as follows:

binding.actionUndo.setOnClickListener(v > binding.editor.undo());
binding.actionRedo.setOnClickListener(v > binding.editor.redo());
binding.actionBold.setOnClickListener(v > binding.editor.setBold());
binding.actionItalic.setOnClickListener(v > binding.editor.setItalic());
binding.actionStrikethrough.setOnClickListener(v > binding.editor.setStrikeThrough());
binding.actionInsertBullets.setOnClickListener(v > binding.editor.setBullets());
binding.actionInsertNumbers.setOnClickListener(v > binding.editor.setNumbers());

To add support for adding links, we need to first setup a click listener and show a dialog on click:

binding.actionInsertLink.setOnClickListener(v -> {
   if (linkDialog == null) {
       createLinkDialog();
   }
   linkDialog.show();
});

The above listener is using a linkDialog, which is initialized in the method below, as follows:

We are first dynamically creating a LinearLayout, and then 2 EditTextViews, and then adding those views to the LinearLayout. Finally we build the AlertDialog as usual and set the Dialog’s view to the LinearLayout we created.

private void createLinkDialog() {
   LinearLayout layout = new LinearLayout(this);
   layout.setOrientation(LinearLayout.VERTICAL);
   final EditText text = new EditText(this);
   text.setHint(getString(R.string.text));
   layout.addView(text);
   final EditText link = new EditText(this);
   link.setHint(getString(R.string.insert_url));
   layout.addView(link);

   linkDialog = new AlertDialog.Builder(this)
      .setPositiveButton(getString(R.string.create), (dialog, which) -> {
           binding.editor.insertLink(link.getText().toString(), text.getText().toString());
      })
      .setNegativeButton(getString(R.string.cancel), (dialog, which) -> {
          dialog.dismiss();
      })
      .setView(layout)
      .create();
}

Here’s how the result looks like:

Resources

Continue Reading

Adding device names’ support for check-ins to Open Event Server

The Open Event Server provides backend support to Open Event Organizer Android App which is used to check-in attendees in an event. When checking in attendees, it is important for any event organizer to keep track of the device that was used to check someone in. For this, we provide an option in the Organizer App settings to set the device name. But this device name should have support in the server as well.

The problem is to be able to add device name data corresponding to each check-in time. Currently attendees model has an attribute called `checkin-times`, which is a csv of time strings. For each value in the csv, there has to be a corresponding device name value. This could be achieved by providing a similar csv key-value pair for “device-name-checkin”.

The constraints that we need to check for while handling device names are as follows:

  • If there’s `device_name_checkin` in the request, there must be `is_checked_in` and `checkin_times` in the data as well.
  • Number of items in checkin_times csv in data should be equal to the length of the device_name_checkin csv.
  • If there’s checkin_times in data, and device-name-checkin is absent, it must be set to `-` indicating no set device name.
if ‘device_name_checkin’ in data and data[‘device_name_checkin’] is not None:
  if ‘is_checked_in’ not in data or not data[‘is_checked_in’]:
       raise UnprocessableEntity(
           {‘pointer’: ‘/data/attributes/device_name_checkin’},
           “Attendee needs to be checked in first”
       )
  elif ‘checkin_times’ not in data or data[‘checkin_times’] is None:
      raise UnprocessableEntity(
          {‘pointer’: ‘/data/attributes/device_name_checkin’},
           “Check in Times missing”
      )
  elif len(data[‘checkin_times’].split(“,”)) != len(data[‘device_name_checkin’].split(“,”)):
     raise UnprocessableEntity(
           {‘pointer’: ‘/data/attributes/device_name_checkin’},
           “Check in Times missing for the corresponding device name”
         )
 if ‘checkin_times’ in data:
   if ‘device_name_checkin’ not in data or data[‘device_name_checkin’] is None:
       data[‘device_name_checkin’] = ‘-‘

The case is a little different for a PATCH request since we need to check for the number of items differently like this:

if ‘device_name_checkin’ in data and data[‘device_name_checkin’] is not None:
            if obj.device_name_checkin is not None:
               data[‘device_name_checkin’] = ‘{},{}’.format(obj.device_name_checkin, data[‘device_name_checkin’])                                                   
            if len(data[‘checkin_times’].split(“,”)) != len(data[‘device_name_checkin’].split(“,”)):
               raise UnprocessableEntity(
                   {‘pointer’: ‘/data/attributes/device_name_checkin’},
                   “Check in Time missing for the corresponding device name”)

Since we expect only the latest value to be present in a PATCH request, we first add it to the object by formatting using:

'{},{}'.format(obj.device_name_checkin, data['device_name_checkin'])

and then compare the length of the obtained CSVs for check in times and device names, so that corresponding to each check in time, we have either a device name or the default fill in value ‘-’.

That’s all. Read the full code here.

Requests and Responses:

Resources

  1. SQLAlchemy Docs
    https://docs.sqlalchemy.org/en/latest/
  2. Alembic Docs
    http://alembic.zzzcomputing.com/en/latest/
  3. Flask REST JSON API Classical CRUD operation
    https://flask-rest-jsonapi.readthedocs.io/en/latest/quickstart.html#classical-crud-operations
Continue Reading

Adding Events’ Payment Preferences to Eventyay Organizer Android App

The Open Event Organizer Android App allows creating events from the app itself. But organizers had to enter the payment information every time. To solve this problem, the PR#1058 was opened which saves the Organizers’ payment preferences in Event Settings.

The Open Event project offers 5 types of payment options:

Online:
1. Paypal
2. Stripe

Offline:
3. Cash payment
4. Bank Transfer
5. Cheques

Each of the above need the payment specific details to be saved. And stuffing all of them into a single Event Settings screen isn’t a good option. Therefore the following navigation was desired:

Event Settings -> Payment Preferences -> All options with their preferences

Android Developer guide references a simple method to achieve the above, which is by using nested preference screens. But unfortunately, there’s a bug in the support library and it cannot be implemented with  PreferenceFragmentCompat

So we had to apply a hack to the UI. We set an OnPreferenceClickListener as follows:

public class EventSettingsFragment extends PreferenceFragmentCompat {
   …
   @Override
   public void onCreatePreferencesFix(@Nullable Bundle bundle, String rootKey) {
       …

       findPreference(“payment_preferences”).setOnPreferenceClickListener(preference -> {
           FragmentTransaction transaction = getFragmentManager().beginTransaction();
           transaction
               .replace(R.id.fragment_container, PaymentPrefsFragment.newInstance())
               .addToBackStack(null)
               .commit();
           return true;
       });
   }
   …
}

Once the preference item “Payment Preferences” is clicked, we initiate a fragment transaction opening the Payment Preferences screen, and add it to the fragment back stack.

For each payment option, we have two things to consider:

  1. Is that payment option supported by the organizer?
  2. If yes, we need to store the necessary details in order to direct the payment to the organizer.

We are also keeping track of whether the organizer wants to keep using the same payment preferences for future events as well. This way we save the organizer’s effort of entering payment details for each new event.

<?xml version=“1.0” encoding=“utf-8”?>
<PreferenceScreen xmlns:android=“http://schemas.android.com/apk/res/android”>

   <CheckBoxPreference
       android:key=“use_payment_prefs”
       android:title=“@string/use_payment_prefs”
       android:summaryOn=“@string/using_payment_preferences”
       android:summaryOff=“@string/not_using_payment_preferences”
       android:defaultValue=“false” />

   <PreferenceCategory
       android:title=“Bank Transfer”>
       <CheckBoxPreference
           android:key=“accept_bank_transfers”
           android:title=“@string/accept_payment_by_bank_transfer”
           android:defaultValue=“false”/>

       <EditTextPreference
           android:key=“bank_details”
           android:title=“@string/bank_details” />
   </PreferenceCategory>
   …

</PreferenceScreen>

Now the only thing remaining is to set payment preferences once the Event Creation form is opened. Hence the following method is called in  CreateEventPresenter  sets the payment details for each option that the organizer has already saved the information for. It does this by using constants named like PREF… all declared in the  Constants.java  file.

using a custom Preference class which abstracts away some boilerplate code for us.

   public void setPaymentPreferences(Preferences preferences) {

       if (preferences.getBoolean(PREF_USE_PAYMENT_PREFS, false)) {
           
           event.setCanPayByBank(
               preferences.getBoolean(PREF_ACCEPT_BANK_TRANSFER, false)
           );
           event.setBankDetails(
               preferences.getString(PREF_BANK_DETAILS, null)
           );
           …
           getView().setPaymentBinding(event);
       }
   }

This is how the result looks like:

Resources

Continue Reading

Adding chrome custom tabs support for native browsing in Eventyay Organizer App

In Eventyay Organizer App when a user taps a URL, we face a choice: either launch a browser, or build our own in-app browser using WebViews.

Both options present challenges — launching the browser is a heavy context switch that isn’t customizable, while WebViews don’t share state with the browser and add maintenance overhead.

Chrome Custom Tabs give apps more control over their web experience, and make transitions between native and web content more seamless without having to resort to a WebView.

The first step for a Custom Tabs integration is adding the Custom Tabs Support Library to your project. Open your build.gradle file and add the support library to the dependency section. One must remember that Chrome Custom Tabs is not an Open Source library, so we are here including the  playStoreImplementation  as opposed to implementation, so that our FDroid build doesn’t fail.

Adding the dependency in build.gradle(app-level) in the project:

dependencies {
   //Other dependencies

   // Chrome Custom Tabs
   playStoreImplementation ‘com.android.support:customtabs:${versions.chromeCustomTabs}
}

 

And add this to  versions.gradle  file:

versions.chromeCustomTabs=‘27.1.0’

The UI Customizations are done by using the  CustomTabsIntent  and the CustomTabsIntent.Builder  classes; the performance improvements are achieved by using the  CustomTabsClient  to connect to the Custom Tabs service, warm-up Chrome and let it know which urls will be opened.

Since we need this to feature to be available for each place in the app that redirects to an external link, we must create it in the utility class of the project. Let’s name this class as BrowserUtils and do it as follows:

public final class BrowserUtils {
    private BrowserUtils() {
   }
    public static void launchUrl(Context context, String url) {
       CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
       CustomTabsIntent customTabsIntent = builder.build();
       customTabsIntent.launchUrl(context, Uri.parse(url));
   }
}

Now all we need to do is replace the old method to create an intent:

findPreference(getString(R.string.privacy_policy_key)).setOnPreferenceClickListener(preference -> {
           Intent intent = new Intent(Intent.ACTION_VIEW);
           intent.setData(Uri.parse(PRIVACY_POLICY_URL));
           startActivity(intent);
           return true;
       });

with this call to the utility method:

findPreference(getString(R.string.privacy_policy_key)).setOnPreferenceClickListener(preference -> {
           BrowserUtils.launchUrl(getContext(), PRIVACY_POLICY_URL);
           return true;
       });

We can also add animation or customize the color of the toolbar, add action buttons or add animations like this:

builder.setToolbarColor(colorInt);
builder.setStartAnimations(this, R.anim.slide_in_right, R.anim.slide_out_left);

builder.setExitAnimations(this, R.anim.slide_in_left, R.anim.slide_out_right);

Here’s the result:

Resources

Continue Reading

Using Transitions API with Email Validation in Open Event Organizer Android App

Transitions in Material Design apps provide visual continuity. As the user navigates the app, views in the app change state. Motion and transformation reinforce the idea that interfaces are tangible, connecting common elements from one view to the next.

In the Open Event Organizer Android App, we need a transition from the Get Started screen such that, if the user email is registered, we transition to the Login Screen otherwise, we Transition to the Sign Up Screen. And the transition should be such that the email field is continuously visible. One more condition is that, if the email field is even varied by one character, we need to transition back to the Get Started Screen.

To implement this, we need to use Shared Elements from the Android Transitions API.

What are shared elements?

A shared element transition determines how views that are present in two fragments transition between them. For example, an image that is displayed on an ImageView on both Fragment A and Fragment B transitions from A to B when B becomes visible.

Fade transition is used to fade a view and ChangeBounds transition is used to move a view without changing its size.

To make a transition, we use the setupTransitionAnimations() function like this:

(Note that we have created our own Fade and ChangeBounds transitions and not using XML)

   public void setupTransitionAnimation(Fragment fragment) {
       Fade exitFade = new Fade();
       exitFade.setDuration(100);
       this.setExitTransition(exitFade);
       fragment.setReturnTransition(exitFade);

       ChangeBounds changeBoundsTransition = new ChangeBounds();
       fragment.setSharedElementEnterTransition(changeBoundsTransition);

       Fade enterFade = new Fade();
       enterFade.setStartDelay(300);
       enterFade.setDuration(300);
       fragment.setEnterTransition(enterFade);
       this.setReenterTransition(enterFade);
   }

Now, in order to detect, if the email field is touched and even changed by one character, we use the TextWatcher like this:

       binding.emailLayout.getEditText().addTextChangedListener(new TextWatcher() {

           @Override
           public void beforeTextChanged(CharSequence s, int start, int count, int after) {
               //do nothing
           }

           @Override
           public void onTextChanged(CharSequence s, int start, int before, int count) {
               if (start != 0) {
                   sharedViewModel.setEmail(s.toString());
                   getFragmentManager().popBackStack();
               }
           }

           @Override
           public void afterTextChanged(Editable s) {
               //do nothing

           }

       }     

So basically, the moment the text is changed, we add the changed email to the SharedViewModel so that it can be used in the other fragments, and then to start the transition, we pop the back stack using getFragmentManager().popBackStack();

This is what the result looks like:

Resources

  • Google – Android developer blog post:

https://android-developers.googleblog.com/2018/02/continuous-shared-element-transitions.html

Continue Reading

Add check-in restrictions to Open Event Organizer App

The Open Event Organizer Android App has the ability to scan and check-in attendees holding different ticket types for an event. But often there are cases when the attendees holding a particular ticket type need to be check-in restricted. It can be because of reasons such as facilitating entry of premium ticket holders before general ticket holders, or not allowing general ticket holders in a VIP queue.

To facilitate this, we have a field called ‘is-checkin-restricted’ for the entity Ticket. So when it is set to true, any check ins for the holder of that particular ticket type will be restricted. Let’s look at how this was implemented in the Orga App.

This is what we want to achieve:

Even though we needed it to be present in the settings screen, we needed it to be dynamic in nature as the types of tickets are themselves dynamic. This meant that we couldn’t achieve this using the plain old preference themes. We must create a whole new fragment for it and try to make it as similar to a preference theme as possible.

We need the following to create a dynamic tickets fragment:

  1. The fragment itself, which should implement the interfaces:  Progressive, Erroneous  to show progress and error.
  2. An Adapter and a ViewHolder
  3. A ViewModel

The fragment CheckinRestriction is similar to the TicketsFragment for the most part except for the part where we need to restrict check in. In the fragment we are providing a checkbox at the top to restrict check-in for all ticket types. So we need to setup click listeners not just for the checkbox, but for the whole view as well, like this:

binding.restrictAll.setOnClickListener(v -> {
       restrictAll(!binding.restrictAllCheckbox.isChecked());
   });
binding.restrictAllCheckbox.setOnClickListener(v -> {
       //checkbox already checked
       restrictAll(binding.restrictAllCheckbox.isChecked());
   });

The restrictAll() method restricts check-in for all ticket types by updating the view and updating the tickets using the ViewModel:

private void restrictAll(boolean toRestrict) {
   binding.restrictAllCheckbox.setChecked(toRestrict);
   ticketSettingsViewModel.updateAllTickets(toRestrict);
   ticketsAdapter.notifyDataSetChanged();
}

It’s also important to note here how we are handling the clicks in the ViewHolder for each ticket item:

public void bind(Ticket ticket) {
   binding.setTicket(ticket);
   View.OnClickListener listener = v -> {
       ticket.isCheckinRestricted = ticket.isCheckinRestricted == null || !ticket.isCheckinRestricted;
       binding.ticketCheckbox.setChecked(ticket.isCheckinRestricted);
       updateTicketAction.push(ticket);
       binding.executePendingBindings();
   };
   itemView.setOnClickListener(listener);
   binding.ticketCheckbox.setOnClickListener(listener);
}

A method that is run each time in order to check if all the tickets are restricted and then accordingly tick the ‘restrict-all’ box.

private void checkRestrictAll() {
   if (ticketSettingsViewModel.getTickets() == null) {
       return;
   }
    boolean restrictAll = true;
    for (Ticket ticket : ticketSettingsViewModel.getTickets().getValue()) {
       if (ticket.isCheckinRestricted == null || !ticket.isCheckinRestricted) {
           restrictAll = false;
           break;
       }
   }
   binding.restrictAllCheckbox.setChecked(restrictAll);
}

This is all of the code we need apart from the boilerplate code in order to successfully build a check-in-restrictions fragment.

Read more of the code here

Resources:

Continue Reading

Implementing bottom navigation view in Organizer App

The state of the app before merging the pull request: #1009 was a simple RecyclerView with StickyHeaders from FastAdapter library.

We needed the app to look more like EventBrite so that we could help the users smoothly transition from EventBrite to Open Event Organizer Android App.

Eventbrite was using Tabs with View Pagers, which is a lot tedious because of its bugginess. View Pagers are buggy and would further require unnecessary architectural changes and additional classes for the Event-list module. This issue was raised in the last GSoC as well, but developers settled with using a single list view.

Using View Pagers requires managing presenters for each of the fragments being used.

So my mentor wanted us to use BottomNavigationView instead. In fact, this was the best solution we could come up with, and we’ll see why:

Step 1: Add BottomNavigationView to the layout:

<android.support.design.widget.BottomNavigationView
           android:id=“@+id/bottom_navigation”
           android:layout_width=“match_parent”
           android:layout_height=“wrap_content”
           android:layout_gravity=“bottom”
           android:background=“@android:color/white”
           app:itemIconTint=“@drawable/bottom_nav_selector”
           app:itemTextColor=“@drawable/bottom_nav_selector”
           app:menu=“@menu/event_list_bottom”
           app:layout_insetEdge=“bottom”/>

The above code for the layout is added to the end of fragment_event_list.xml and is to just add the Bottom Navigation layout for the xml. The attribute   app:menu takes the reference to the menu resource layout for the menu items to be used, which we’ll setup next:

Step 2: Declaring menu resource file for the BottomNavigationView:

The menu resource file is used to specify what items we need in the BottomNavigationView.

<?xml version=“1.0” encoding=“utf-8”?>
<menu xmlns:android=“http://schemas.android.com/apk/res/android”>
   <item android:id=“@+id/action_live”
       android:title=“@string/event_state_live”
       android:icon=“@drawable/ic_time”/>
   <item android:id=“@+id/action_upcoming”
       android:title=“@string/event_state_upcoming”
       android:icon=“@drawable/ic_clock”/>
   <item android:id=“@+id/action_past”
       android:title=“@string/event_state_past”
       android:icon=“@drawable/ic_info”/>
</menu>

Step 3: Implement Filterable in Adapter:

Filterable is an interface which is generally used with an adapter when there are many different values possible for the adapter.

class EventsListAdapter extends RecyclerView.Adapter<EventsListAdapter.EventRecyclerViewHolder> implements StickyRecyclerHeadersAdapter<HeaderViewHolder>, Filterable {

Filterable requires overriding the method: getFilter() which as the name suggests, provide the filter logic to be used.

@Override
public Filter getFilter() {
   return new Filter() {
       @Override
       protected FilterResults performFiltering(CharSequence constraint) {
           selectedEvents.clear();
           for (Event event : events) {
               try {
                   String category = DateService.getEventStatus(event);
                   if (constraint.toString().equalsIgnoreCase(category))
                       selectedEvents.add(event);
               } catch (ParseException e) {
                   Timber.e(e);
               }
           }
           return null;
       }
        @Override
        protected void publishResults(CharSequence constraint, FilterResults results) {
           notifyDataSetChanged();
        }
    };
}

Step 4: Setting up the BottomViewNavigation:

We have already set up the binding so we can reference the bottomNavigationView directly and setup the OnNavigationItemSelectedListener as follows:

binding.bottomNavigation.setOnNavigationItemSelectedListener(
           item -> {
               switch (item.getItemId()) {
                   case R.id.action_live:
                       eventListAdapter.getFilter().filter(“live”);
                       return true;
                   case R.id.action_upcoming:
                       eventListAdapter.getFilter().filter(“upcoming”);
                       return true;
                   case R.id.action_past:
                       eventListAdapter.getFilter().filter(“past”);
                       return true;
                   default:
                      return false;
               }
           });

As can be seen above, we can directly filter according to the string parameters we are passing and the logic is being handled in the getFilter() method.

This is the result of the making the above changes:

The best benefits of using  BottomNavigationView  was that we didn’t even had to touch the presenter code to handle event list, or create any new fragments. This was one of the cleanest possible solutions, but now we have a pager implemented in the app, which seems to give a little bit more visual appeal.

References:

Open Event Organizer App – Pull Request #1009
https://github.com/fossasia/open-event-orga-app/pull/1009

Continue Reading

Using DialogFragment to show sales data in Open Event Organizer App’s Events List

In the Open Event Organizer Android App The events list shows the list of events, but organizers often need to compare sales of different events fast. Until now they had to select each and every item and go to the dashboard to see the sales. This is about to change in the Pull Request #1063. It provides an intuitive way to show ticket sales by showing a dialog box upon long pressing event list items.

To implement it, we needed a BaseDialogFragment class which would implement Injectable and handle the presenter life cycle for us.

public class BaseDialogFragment<P extends BasePresenter> extends DialogFragment implements Injectable {

BaseDialogFragment class is very similar to the BaseFragment class, except that it extends DialogFragment instead of Fragment, And the new class SalesSummaryFragment is also similar to any other fragment class we are using.

When an item in the events list is clicked, the long click listener for the events’ list adapter opens the SalesSummaryFragment for the corresponding event, and when the data completes loading it calls ItemResult#showResult and binds the event with the data.

@Override
public void showResult(Event event) {
    binding.setEvent(event);
    binding.executePendingBindings();
}

The  SalesSummaryPresenter  is the almost the same as the TicketsPresenter, since it is loading ticket sales. In its onStart() method, it calls the method  loadDetails()  which on completion calls  analyseSoldTickets()  on  ticketAnalyser, which basically updates an event’s analytics after calculations.

public class SalesSummaryPresenter extends AbstractDetailPresenter<Long, SalesSummaryView> {
    …

    public void loadDetails(boolean forceReload) {
        …
        getEventSource(forceReload)
            …
            .subscribe(attendees -> {
                this.attendees = attendees;
                ticketAnalyser.analyseSoldTickets(event, attendees);
            }, Logger::logError);
    }

   …

    @Override
    public void showResult(Event event) {
        binding.setEvent(event);
        binding.executePendingBindings();
    }
}

For  SalesSummaryFragment , we are using the layout  fragment_sales_summary.xml. And this layout mainly makes use of the layout ticket_analytics_item.xml which is a layout designed to produce the three circular views to show the data.

<include
    layout="@layout/ticket_analytics_item"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    bind:color='@{"red"}'
    bind:completed="@{event.analytics.soldDonationTickets}"
    bind:ticketName="@{@string/ticket_donation}"
    bind:total="@{event.analytics.donationTickets}" />

This is how the result looks like:

References:
Open Event Orga Android App: Pull Request #1063:
https://github.com/fossasia/open-event-orga-app/pull/1063

Codepath guides: Using Dialog Fragment
https://github.com/codepath/android_guides/wiki/Using-DialogFragment

Continue Reading

Using SharedViewModel for data persistence in multiple Fragments in Open Event Organizer Android App

Google announced the new architectural pattern Model-View-View-Model (or MVVM), which has gained a lot of popularity for increased simplicity. The Open Event Organizer Android App is currently (at the time of writing this blog) using Model-View-Presenter (MVP) architecture for the most part. Only the Login module had been converted to MVVM, but it wasn’t using any Shared View Model.

Let’s look at the problem at hand:

For the Login fragment, there’s an email field for user’s email in every fragment of the auth module. But there’s the use case where users can bounce around different fragment screens in order to perform different operations like login, signup, forgot password and enter new password.

A good User Experience is at the core of every successful product. Users don’t like to spend time doing trivial things like registration and they hate to retype emails again and again.

To solve the problem, the approach that I first used was, using interfaces to tell each and every component when the fragment opens and send the email text through the interface. I spent quite some time implementing the interfaces right and then opened a PR. It was around 200 lines addition at first, and I also received 2-3 requested changes from my project mentor.

Defining the interface:

The interface  EmailIdTransfer  was defined in the  LoginFragment  as follows:

public class LoginFragment extends BaseFragment implements LoginView, EmailIdTransfer {

Using the interface:

getPresenter().getEmailId().setEmail(
((LoginFragment.EmailIdTransfer) getContext()).getEmail()

Imagine setting up the   EmailIdTransfer  interface for each Fragment, and handling their updation and reception.

As you can see, this is a bad solution for something as trivial as the problem at hand! The better solution which stuck me (from Google I/O) afterwards was to use ViewModels. This obviously wasn’t MVP, but since we had to eventually move to MVVM, I dug a little more about it and found out that there’s also a SharedViewModel.

SharedViewModel is basically a View Model that every view of a module shares. It is not very different from ViewModel.

Here’s how to set it up:

public class SharedViewModel extends ViewModel {

   private final MutableLiveData<String> email = new MutableLiveData<>();

   public void setEmail(String email) {
       this.email.setValue(email);
   }

   public LiveData<String> getEmail() {
       return email;
   }
}

The MVVM architecture uses LiveData instead of Observables.

Android LiveData is a variant of the original observer pattern, with the addition of active/inactive transitions. As such, it is very restrictive in its scope. RxJava provides operators that are much more generalized.

Now to use this SharedViewModel in each fragment, we need:

sharedViewModel = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);

sharedViewModel.getEmail().observe(this, email -> binding.getForgotEmail().setEmail(email));

This is the initial setup for the the fragment.

private void openLoginPage() {

   …
   sharedViewModel.setEmail(binding.getUser().getEmail());
   …
}

This is needed to be done for every fragment which needs to set and use the common email.

This was a dive into using LiveData<>, ViewModel and how to use a SharedViewModel.

This is how the result looks like:

References:

Continue Reading

Supporting Toolbar Options – Edit and multiple deletions – in Open Event Organizer App

The Open Event Organizer Android App was supporting deletion for only one item, and didn’t support the edit option. It needed to support multiple deletions for items and the edit operation. This PR and hence the blog covers details on how to handle multiple deletions and implementing the edit option.

Here are the steps I followed to implement these options from scratch:

Step 1: Create toolbar menu

First create a menu for toolbar with the desired toolbar actions (delete and edit in our case)

<?xml version=“1.0” encoding=“utf-8”?>
<menu xmlns:android=“http://schemas.android.com/apk/res/android”
   xmlns:app=“http://schemas.android.com/apk/res-auto”>

   <item android:id=“@+id/del”
       android:title=“Delete”
       android:icon=“@drawable/ic_delete”
       android:visible=“false”
       app:showAsAction=“ifRoom”>
   </item>

   <item android:id=“@+id/edit”
       android:title=“Update”
       android:icon=“@drawable/ic_edit”
       android:visible=“false”
       app:showAsAction=“ifRoom”>
   </item>

</menu>

Step 2: Setup helper options:

We need to keep track of which items have been selected and also be able to get the count of these selected items many times. Therefore, we need to setup the following helper methods  isTrackSelected()  and countSelected()

Both these methods make use of the HashMap:

private final Map<Long, ObservableBoolean> selectedTracks = new HashMap<>();

Every time a method uses  isTrackSelected() , it checks whether or not there is an entry of that track in the HashMap selectedTracks. If not, it adds the entry and returns the corresponding default set ObservableBoolean.

public ObservableBoolean isTrackSelected(Long trackId) {
       if (!selectedTracks.containsKey(trackId))
           selectedTracks.put(trackId, new ObservableBoolean(false));

       return selectedTracks.get(trackId);
   }

   private int countSelected() {
       int count = 0;
       for (Long id : selectedTracks.keySet()) {
           if (selectedTracks.get(id).get())
               Count++;
       }
       return count;
   }

Step 3: Handling clicks

The logic to be followed had to be consistent with existing popular apps, which users are habitual of using. It thus had to be intuitive. And so here’s the logic we follow:

Click -> Opens intent (if toolbar not already active)

Click -> Selects item (if toolbar is active and item is not already selected)

Click -> Deselects item (if item already selected)

Click -> Deselects item and de-activates toolbar (if the already selected item was the only one)

Long Click -> Activates toolbar and selects item (if toolbar not already active)

Long Click -> Selects item (if toolbar already active)

> Handling single clicks:

Following the described logic, we see if a toolbar is not active, we simply open the detailed view of the already selected item.

public void click(Long clickedTrackId) {
       if (isToolbarActive) {

           if (countSelected() == 1 && isTrackSelected(clickedTrackId).get()) {
               selectedTracks.get(clickedTrackId).set(false);
               resetToolbarDefaultState();
           } else if (countSelected() == 2 && isTrackSelected(clickedTrackId).get()) {
               selectedTracks.get(clickedTrackId).set(false);
               getView().changeToolbarMode(true, true);
           } else if (isTrackSelected(clickedTrackId).get())
               selectedTracks.get(clickedTrackId).set(false);
           else
               selectedTracks.get(clickedTrackId).set(true);

           if (countSelected() > EDITABLE_AT_ONCE)
               getView().changeToolbarMode(false, true);

       } else
           getView().openSessionsFragment(clickedTrackId);
   }

> Handling long clicks:

If the toolbar is active, long clicks behave same as simple clicks. Otherwise, we first add them to the selectedTracks, set isToolbarActive to true and change toolbar mode to activate edit and delete options.

public void longClick(Track clickedTrack) {
       if (isToolbarActive)
           click(clickedTrack.getId());
       else {
           selectedTracks.get(clickedTrack.getId()).set(true);
           isToolbarActive = true;
           getView().changeToolbarMode(true, true);
       }
   }

   public void resetToolbarDefaultState() {
       isToolbarActive = false;
       getView().changeToolbarMode(false, false);
  }

Step 4: Add relevant methods in presenter

We need to add the deleteSelectedTracks() method to the presenter so that we can delete all the tracks selected for deletion.

This methods only iterates from iterable selectedTracks.entrySet() and then executes the already existing deleteTrack() method from the presenter

private void deleteTrack(Long trackId) {
        trackRepository
            .deleteTrack(trackId)
            .compose(disposeCompletable(getDisposable()))
           .compose(progressiveErroneousCompletable(getView()))
            .subscribe(() -> {
               getView().showTrackDeleted(“Track Deleted”);
               loadTracks(true);
               selectedTracks.remove(trackId);
               Logger.logSuccess(trackId);
           }, Logger::logError);
   }

   public void deleteSelectedTracks() {
       Observable.fromIterable(selectedTracks.entrySet())
           .compose(dispose(getDisposable()))
           .compose(progressiveErroneous(getView()))
           .doFinally(() -> {
               getView().showMessage(“Tracks Deleted”);
               resetToolbarDefaultState();
           })
           .subscribe(entry -> {
               if (entry.getValue().get()) {
                   deleteTrack(entry.getKey());
               }
            }, Logger::logError);
    }

This is how the result looks like:

Implementation in Open Event Organizer Android App

Resources

Continue Reading
  • 1
  • 2
Close Menu