ButterKnife Nasıl Çalışır?

Bu yazımızda, bir View Injector kütüphanesi olan ButterKnife’ın nasıl çalıştığını inceleyeceğiz.

Square‘da çalışan Jake Wharton son dönemde geliştirdiği kütüphanelerle Android dünyasının adeta zirvesine yerleşti. ActionbarSherlock, Picasso, Retrofit ve ButterKnife gibi kütüphaneler, yazılım geliştiricilerin hayatını epey kolaylaştırıyor. Bundan dolayı Jake Wharton’a “Android’in Taçsız Prensi” diyebiliriz. Bu yazımızda, bir View Injector kütüphanesi olan ButterKnife’in nasıl çalıştığını inceleyeceğiz.

 

Öncesi – sonrası

Bilindiği üzere ButterKnife bir View Injector kütüphanesidir. Bir başka deyişle, önyüzde tanımladığımız view bileşenlerini, annotation’lar kullanarak projemize dâhil etmemize olanak sağlıyor. Sadece view’leri değil, aynı zamanda projemizde kullandığımız String, Drawable, Color ve Dimen gibi kaynakları da aynı yöntemle pratik bir şekilde projemize ekleyip bizi kod tekrarı yapmaktan kurtarıyor.

class ExampleActivity extends Activity {
    @Bind(R.id.title) TextView title;
    @Bind(R.id.subtitle) TextView subtitle;
    @Bind(R.id.footer) TextView footer;

    @Override public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.simple_activity);
        ButterKnife.bind(this);
        // TODO Use fields...
    }
}

Görüldüğü üzere findViewById() metodunu tekrar tekrar kullanmak zorunda kalmıyoruz. Peki, ButterKnife’ın çalışma prensibi nedir?

 

Ne sihirdir ne keramet! Annotation Processing’de alamet!

ButterKnife’ın arkasında yatan güç, Annotation Processing’dir. Annotation Processing, derleme işleminin aşamalarından biridir. JDK’nın belgesine göz atarsanız, derleme sürecini anlatan bölümde bunu açıkça görebilirsiniz. (OpenJDK Belgesi)

Annotationlar derleme zamanında tespit edilir ve normal kod satırlarına çevrilirler. Bir başka deyişle, runtime dediğimiz gerçek çalışma zamanında annotation diye bir şey yoktur. Projemizi derlediğimizde, Annotation Processor devreye girer, projedeki tüm Java sınıflarını tarayıp, annotationları bulur ve normal kod satırlarına çevirir. Üretilen bu yeni kod, normal bir Java sınıfı gibi yeniden derlenir. Annotationlar sadece yazılım geliştirme sürecini kolaylaştırmak için görünürde vardır, derleme zamanının ötesine geçemezler. Unutulmamalıdır ki, Annotation Processorlar sadece annotationları normal satırlara çevirmeye yarar. Derleme anında, sınıflara yeni metodlar eklemek gibi değişiklikler yapamazlar.

Java’da kendi annotationlarımızı yazma olanağımız var. Bunun için aynı zamanda bu annotationları işleyecek bir Annotation Processor yazmamız gerekli. ButterKnife için yapılan da tam olarak bundan ibaret. Bir Annotation Processor yazmak için AbstractProcessor.java sınıfını extend eden bir sınıf yazmamız gereklidir. AbstractProcessor.java içinde tanımlı olan ve bizim kullanacağımız process() sınıfı, asıl işin yapıldığı yer.

Bu bağlantıya tıklayarak ButterKnife için yazılan ButterKnifeProcessor.java sınıfını inceleyebilirsiniz.

 

ButterKnight algoritması

Android projenizi ButterKnife kullanarak yazdınız ve Build düğmesine basıp, projeyi derlediniz. Arka planda olanlar kabaca şu şekilde:

  • Derleyici, ButterKnife’in annotation processorunun process() metodunu çağırır.
  • Bu metot içinde, projenizdeki tüm Java sınıfları taranır ve ButterKnife annotationları nerelerde kullanılmış, tespit edilir.
  • Annotation kullanılmış bir sınıf bulunduğu zaman, yeni bir Java dosyası üretilir: <Sınıf_Adi>$$ViewInjector.java (Bildiğimiz bir Java sınıfı)
  • Bu yeni ViewInjector sınıfı içindeki annotationlar, bizim bildiğimiz eski stil kodlarla yer değiştirilir. Kısacası, findViewById() ve view.setOnClickListener() gibi satırlar tam bu noktada projemize eklenir.
  • Son olarak da ButterKnife.inject(this) satırı çağırıldığında, üretilen tüm VievInjector dosyalarının inject() metotları çağrılır ve bu dosyalar derlenmeye başlanır.

Yukarıdaki verdiğimiz kod derlenirken, ButterKnife tarafından üretilen kodu aşağıda görebiliyoruz:

public class ExampleActivity$$ViewBinder<T extends com.lgvalle.samples.ui.ExampleActivity> implements ViewBinder<T> {
    @Override public void bind(final Finder finder, final T target, Object source) {
        View view;
        view = finder.findRequiredView(source, 21313618, “field ‘title’”);
        target.title = finder.castView(view, 21313618, “field ‘title’”);
        view = finder.findRequiredView(source, 21313618, “field ‘subtitle’”);
        target.subtitle = finder.castView(view, 21313618, “field ‘subtitle’”);
    }

Son olarak tekrar belirtelim; siz de kendi annotationlarınızı geliştirebilir ve ButterKnife gibi bir View Injector kütuphanesi yazabilirsiniz. ButterKnife hakkında daha fazla bilgi ve kaynak kodları için aşağıdaki bağlantıyı inceleyebilirsiniz:

Advertisements

Android 6.0 ve Yenilenen İzin İsteme Mekanızması

Bu yazımızda Android 6.0 ile birlikte yenilenen Request Permission yani telefonun bazı özelliklerini kullanabilmek için izin isteme yapısını inceleyeceğiz.

 

android-image.png

Bildiğiniz gibi uygulama içinde kullanmak istediğimiz özellikler için AndroidManifest.xml dosyasına gerekli izinleri tanımlamamız yetiyordu. Kullanıcı Google Play’den uygulamayı indiriken, istediğimiz izinler karşısında listeleniyor ve uygulamayı  indirmek için  bu izinleri kabul etmek zorunda kalıyor. Sonrasında ise uygulama bu özellikleri tekrar izine ihtiyaç duymadan defalarca kez kullanabiliyordu. Fakat Android 6.0 ile bu izin yapısı biraz değişikliğe uğradı. Şöyle ki, Google kullanılan izinleri NORMAL İZİNLER ve TEHLİKLELİ İZİNLER olarak ikiye ayırdı. (Aslında iki değil dört kategori var fakat ilk ikisi yeterli bizim için.) Tehlikeli olarak tanımlanan türden izinler için AndroidManifest.xml’e tanımlamak yeterli olmuyor artık, uygulama çalışırken izin gereken bir iş yapmak istediğinde anında kullanıcıya sorup, her defasında izin almamız gerekiyor.

Özetle,  tüm izinleri yine AndroidManifest.xml‘e tanımlıyoruz. Beraberinde tehlikeli izin gerektiren işler içinse uygulama çalışırken tekrar izin istemek zorundayız. IPhone kullanan kullanıcılar bu yapıya aşinadır aslında. Çünkü IOS bu izin yapısını yıllardır başarıyla kullanıyor kendi uygulamaları içinde.

Bahsettiğimiz izin kategorilerine giren izinleri aşağıdaki gibi  listeleyebiliriz.

Normal İzinler:  https://developer.android.com/guide/topics/security/normal-permissions.html

Tehlikeli İzinler: https://developer.android.com/guide/topics/security/permissions.html?normal-dangerous

Örneğin internete bağlanmak normal bir izin gerektiriken, rehbere ulaşmak, SD karta dosya yazmak ya da bir numarayı aramak tehlikeli izin kategorisine giriyor.

İZİNLER İÇİN YENİ BİR DESIGN PATTERN

Gel gelelim bu izinleri nasıl isteyeceğimize. İzin istemek ve kullanıcının izin verip vermediğini kontrol etmek için yaklaşık 30 satırlık standart bir kod yazmamız gerekli. Uygulama içinde ortalama on yerde izin talebinde bulunduğumuzu düşünürsek 300 satırlık gereksiz ve kendini tekrar eden bir kod yığını oluşur. Üstelik izin istediğimiz iş ve izin isteme işlerini aynı metodda toplarsak bu Single Responsibility Prensibine de aykırı olacaktır.

Peki Çözüm Nedir?

Malum yazılım dünyasında patternlar, prensipler derken her işi yapmanın adam akıllı yolları var. Bunun için de tavsiye edilen yöntem Runtime Permissions Activity Pattern’dir.  Uzun lafın kısası, abstract bir Actvitiy sınıfı yazıyoruz. Bu abstract sınıf içinde izin istemek ve izinin sonucunu kontrol etmek için gerekli metodlar bulunuyor.  Yazdığımız tüm Activity sınıflarında bu abstract sınıfı extend ediyoruz ve gönül rahatlığı ile kullanabiliyoruz.

 

RuntimePermissionsActivity.java

 

public abstract class RuntimePermissionsActivity extends AppCompatActivity {

    private SparseIntArray mErrorString;

    @Override

    protected void onCreate(@Nullable Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        mErrorString = new SparseIntArray();

    }

    @Override

    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {

        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        int permissionCheck = PackageManager.PERMISSION_GRANTED;

        for (int permission : grantResults) {

            permissionCheck = permissionCheck + permission;

        }

        if ((grantResults.length > 0) && permissionCheck == PackageManager.PERMISSION_GRANTED) {

            onPermissionsGranted(requestCode);

        } else {

            Snackbar.make(findViewById(android.R.id.content), mErrorString.get(requestCode),

                    Snackbar.LENGTH_INDEFINITE).setAction("ENABLE",

                    new View.OnClickListener() {

                        @Override

                        public void onClick(View v) {

                            Intent intent = new Intent();

                            intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);

                            intent.addCategory(Intent.CATEGORY_DEFAULT);

                            intent.setData(Uri.parse("package:" + getPackageName()));

                            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

                            intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);

                            intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);

                            startActivity(intent);

                        }

                    }).show();

        }

    }

    public void requestAppPermissions(final String[] requestedPermissions,

                                      final int stringId, final int requestCode) {

        mErrorString.put(requestCode, stringId);

        int permissionCheck = PackageManager.PERMISSION_GRANTED;

        boolean shouldShowRequestPermissionRationale = false;

        for (String permission : requestedPermissions) {

            permissionCheck = permissionCheck + ContextCompat.checkSelfPermission(this, permission);

            shouldShowRequestPermissionRationale = shouldShowRequestPermissionRationale || ActivityCompat.shouldShowRequestPermissionRationale(this, permission);

        }

        if (permissionCheck != PackageManager.PERMISSION_GRANTED) {

            if (shouldShowRequestPermissionRationale) {

                Snackbar.make(findViewById(android.R.id.content), stringId,

                        Snackbar.LENGTH_INDEFINITE).setAction("GRANT",

                        new View.OnClickListener() {

                            @Override

                            public void onClick(View v) {

                                ActivityCompat.requestPermissions(RuntimePermissionsActivity.this, requestedPermissions, requestCode);

                            }

                        }).show();

            } else {

                /*User has chosen "Never ask again."  So we shouldn't request again.. DO NOTHING*/

            }

        } else {

            onPermissionsGranted(requestCode);

        }

    }

    public abstract void onPermissionsGranted(int requestCode);

}

RuntimePermissionsActivity.java sınıfımız içinde üç tane metod bulunuyor. İlki requestAppPermissions()  isimli metod izin istemek için çağrılması gereken metoddur. Dikkat ederseniz ilk parametresi String değil String dizisidir.(String[]) Yani aynı anda birden fazla izin istemek için kullanılabilir bu metod.

İkinci metod onRequestPermissionsResult()    bizim yazmadığımız, FragmentActivity.java sınıfından miras alıp override ettiğimiz metoddur. Bu metod kullanıcının bize izin verip vermediğini anlayacağımız bir callback yani geri bildirim metodudur.   Eğer kullanıcı bize izin vermişse onPermissionsGranted()    isimli abstract metodu çağırarak, izin isteğinin yapıldığı asıl Activity sınıfına haber veriyoruz. bu metod bu yüzden abstract olarak tanımlandı. Eğer kullanıcı izin vermemişse de elimizden gelen birşey yok. bu özelliği kullanamayacağız demektir.

Sıklıkla yapılan bir hataya dikkat çekmek isterim.  Herhangi bir izin kullanıcıdan red almışsa, bu izin  ikinci kez istendiğinde Android işletim Sistemi, açılan pencereye “Bir daha sorma” şeklinde bir checkbox çıkarır. Kullanıcı bu kısmı işaretlerse, bu izin tekrar istenmemelidir. Bu durum kullanıcıyı rahatsız eder.  Fakat yazılım geliştiriciler genelde bu kontrolü görmezden gelerek bu ayar işaretli olsa da olmasa da izin isteme yoluna giderler. Bu durum kullanıcıyı rahatsız eder.

Kullanıcının bu izin için bir daha rahatsız edilmek istemediğini  requestAppPermissions()   metodu içerisindeki  shouldShowRequestPermissionRationale  değişkeninden anlayabiliyoruz. Eğer false  gelirse kullanıcıyı rahatsız etmiyoruz.

 

Son olarak, bu metodları nasıl çağıracağımıza bakıyoruz. Yazacağımız her Activity sınıfında RuntimePermissionAbstractActivity.java sınıfını extend etmemiz gerekiyor.

 MainActivity.java

public class MainActivity extends RuntimePermissionsActivity {

    private static final int REQUEST_PERMISSION_READ_CONTACTS = 1;

    private static final int REQUEST_PERMISSION_WRITE_EXTERNAL_STORAGE = 2;

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);



               

MainActivity.super.requestAppPermissions(new

                        String[]{Manifest.permission.READ_CONTACTS,}, R.string

                        .runtime_permissions_txt, REQUEST_PERMISSION_READ_CONTACTS);

               

MainActivity.super.requestAppPermissions(new

                        String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, R.string.runtime_permissions_txt, REQUEST_PERMISSION_READ_CONTACTS);

    }

    @Override

    public void onPermissionsGranted(final int requestCode) {

        if (requestCode == REQUEST_PERMISSION_READ_CONTACTS) {

            Toast.makeText(this, "Permissions Received for reading contacts.", Toast.LENGTH_LONG).show();

        } else if (requestCode == REQUEST_PERMISSION_WRITE_EXTERNAL_STORAGE) {

            Toast.makeText(this, "Permissions Received for writing SD card.", Toast.LENGTH_LONG).show();

        }

    }

}

 

Override ettiğimiz  onPermissionsGranted()   metodu,  kabul edilen her izin için tetiklenecek kabul edilmeyenler içinse gereksiz gürültü yapmayacaktır.

Projenin kodlarını Github hesabımızda indirebilir, bedava uygulamanın keyfine varabilirsiniz.

Github : https://github.com/burhanaras/Android-Runtime-Permissions

Teşekkürler.

Burhan ARAS

Activity ve Fragment Çağırmanın Doğru Yöntemi

Bugüne kadar gördüğüm Android projelerinin çoğunda en sık karşılaştığım hata yeni  Activity ve Fragmentların  yanlış çağrılmasıdır.

Activity ve Fragmentlar uygulamanın ekranlarını temsil ettiği için önemli bir yeri vardır. Bu nedenle bu aşamada yapılan yanlış kullanımlar tüm uygulamamızın ileride spagetti koda dönüşmesi yönünde atılan ilk adımlardan biri olur.

Yazılım dünyasında tek bir mutlak doğru yoktur fakat bir işi yapmanın otoritelerce kabul görmüş yolları vardır. Yeni Activity ya da Fragment çağırmanın doğru yollarını görmeden önce yapılan hatalı çağrımları inceleyelim.

 

Hatalı Uygulama 1

HomeActivity.user = user;
Intent intent = new Intent(LoginActivity.this, HomeActivity.class);
startActivity(intent);

 

Özellikle yeni başlayan arkadaşların en sık başvurdukları yöntemdir. Çağıracağınız Activity içerisinde static bir değişken tanımlayıp sonrasında Activity’ i çağırmak. Unutulmamalıdır ki static değişkenler uygulamanın test edilmesini zorlaştırır. Ayrıca bu Activity’ i çağırması gereken client her seferinde bu static değişkene değer vermesi gerektiğini bilemez. Bu nedenle bu yöntemden vazgeçilmelidir.

 

Hatalı Uygulama 2

private static final String KEY_USER = "KEY_USER"; 

Intent intent = new Intent(this, HomeActivity.class); 
intent.putExtra(KEY_USER, user); 
startActivity(intent);

 

İlkine göre daha iyi denebilir ama hala hatalıdır. Çünkü  KEY_USER  isimli String’ in aynısından HomeActivity içerisinde de oluşturmamız gereklidir. Ayrıca HomeActivity’ i çağıran tüm sınıflar içinde de yine bu String tanımlanmalıdır. Gereksiz kod tekrarı, yönetilmesi zor key-value çiftleri… Ayrıca yazım hatası oluşması durumunda hatalara çok açıktır.

 

Hatalı Uygulama 3

Intent intent = new Intent(this, HomeActivity.class);
intent.putExtra(HomeActivity.KEY_USER, user); 
startActivity(intent);

Bir önceki hatadan ders çıkarıyoruz. KEY_USER   isimli String’ i HomeActivity içerisine alıyoruz. Bu durumda HomeActivity’ i çağıran her sınıf içerisinde tanımlı olması gerekmez. Fakat bu durumda da HomeActivity içerisindeki bu String’ e bağımlı hale geliriz. HomeActivity’ i çağırırken key olarak bu String’ i kullanmamız gerektiğini nereden bilebiliriz ki? Bu durum da ileride muhtemel hatalara davetiye çıkaracaktır.

 

Hatalı Uygulama 4

Intent intent = new Intent(this, HomeActivity.class); 
intent.putExtra(Constants.KEY_USER, user); 
startActivity(intent);

Uygulama geliştiriken çok severek oluşturduğumuz, şifre, kullanıcı adı, key gibi ne kadar sabit değer varsa neredeyse hepsini içine yazdığımız meşhur Constants class’ ına hoşgeldiniz.  Hemen her projede vardır mutlaka. Son çare olarak  KEY_USER  String’ ini Constants sınıfı içerisine tanımladık fakat bu malesef doğru değildir. Hangi key’ i çağırmamız gerektiğini  bize birldirmediği gibi bizi bu sınıfa gereksiz şekilde baağımlı kılar.
Nihayet Doğru Uygulama

Nihayetinde doğru yol aslında hiç key kullanmamaktır. Parametre olarak yollayacağımız nesneyi de key kullanmadan, doğrudan yollamaktır. Bu yöntemin adı Static Factory Method‘ dur.  Lafı uzatmadan doğru yolu gösterelim, açıklamasını sonra yaparız.

Intent intent = HomeActivity.newIntent(LoginActivity.this, user); 
startActivity(intent);

Bu  şekilde çağırısak HomeActivity’ i key dediğimiz Stringleri yönetmek zorunda kalmayız. HomeActivity içinde de bize Intent döndüren newIntent() metodunu tanımlıyoruz.

 

public class HomeActivity extends AppCompatActivity {

    private static final String KEY_USER = "KEY_USER";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);

        User user = getIntent().getParcelableExtra(KEY_USER);
        if (user != null) {
            Log.d(TAG,user.toString());
        }
    }
    
    public static Intent newIntent(Activity callerActivity, User user){
        Intent intent=new Intent(callerActivity, HomeActivity.class);
        intent.putExtra(KEY_USER, user);
        return intent;
    }
}

 

Bu yöntemin asıl adı Static Factory Method‘ dur.  Joshua Bloch‘ un yazdığı,  yazılım dünyasındaki en popüler kitaplardan biri olan Effective JAVA kitabında birinci madde olarak anlatılmaktadır. Bu kitabı da mutlaka okumanızı tavsiye ederim.

 

Fragmentlar Çağrılırken

Fragmentlar için de benzer şekilde static factory method kullanarak  parametre gönderebiliyoruz.

    public static BlankFragment newInstance(String param1, String param2) {
        BlankFragment fragment = new BlankFragment();
        Bundle args = new Bundle();
        args.putString(ARG_PARAM1, param1);
        args.putString(ARG_PARAM2, param2);
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            mParam1 = getArguments().getString(ARG_PARAM1);
            mParam2 = getArguments().getString(ARG_PARAM2);
        }
    }

 

Joshua Bloch‘un Effective Java kitabını okumanızı tekrar tavsiye ederim.

Hoşçakalın.